diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 5e3f97b36..000000000 --- a/.babelrc +++ /dev/null @@ -1,24 +0,0 @@ -{ - "presets": [ - [ - "@babel/preset-env", - { - "loose": true - } - ] - ], - "plugins": [ - [ - "@babel/plugin-proposal-class-properties", - { - "loose": true - } - ], - [ - "@babel/plugin-proposal-optional-chaining", - { - "loose": true - } - ] - ] -} diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index d5e9fecc0..000000000 --- a/.browserslistrc +++ /dev/null @@ -1,2 +0,0 @@ -last 2 versions -ie >= 11 diff --git a/.editorconfig b/.editorconfig index 0daf12d6f..bbd49f2d8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,5 +4,8 @@ root = true end_of_line = lf insert_final_newline = true indent_style = space -indent_size = 2 +indent_size = 4 trim_trailing_whitespace = true + +[*.{json,yml}] +indent_size = 2 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..76c1d4af0 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + root: true, + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + rules: { + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'prefer-const': ['error', { destructuring: 'all' }], + }, + ignorePatterns: ['dist/**/*', '*.js'], +}; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index fd9e05cdb..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "ignorePatterns": [ - "*.spec.js", - "DeviceOrientationControls.js", - "StereoEffect.js" - ], - "extends": "airbnb-base", - "parser": "@babel/eslint-parser", - "env": { - "browser": true, - "es6": true - }, - "rules": { - "arrow-body-style": "off", - "class-methods-use-this": "off", - "import/no-cycle": "off", - "import/prefer-default-export": "off", - "key-spacing": "off", - "no-bitwise": "off", - "no-case-declarations": "off", - "no-else-return": "off", - "no-multiple-empty-lines": "off", - "no-param-reassign": "off", - "no-plusplus": "off", - "no-restricted-properties": "off", - "no-underscore-dangle": "off", - "no-useless-constructor": "off", - "no-unused-expressions": "off", - "object-curly-newline": "off", - "prefer-destructuring": "off", - "prefer-template": "off", - "no-mixed-operators": "off", - "no-restricted-globals": "off", - "function-paren-newline": "off", - "function-call-argument-newline": "off", - "prefer-exponentiation-operator": "off", - "arrow-parens": [ - "error", - "as-needed", - { - "requireForBlockBody": true - } - ], - "max-len": [ - "error", - { - "code": 150, - "ignoreComments": true - } - ], - "brace-style": [ - "error", - "stroustrup" - ], - "comma-dangle": [ - "error", - { - "objects": "always-multiline", - "arrays": "always-multiline", - "functions": "never" - } - ], - "padded-blocks": [ - "error", - { - "classes": "always", - "blocks": "never", - "switches": "never" - } - ], - "no-console": [ - "error", - { - "allow": [ - "warn", - "error" - ] - } - ], - "object-shorthand": [ - "error", - "consistent-as-needed" - ], - "no-use-before-define": [ - "error", - { - "functions": false, - "classes": true, - "variables": true - } - ], - "quote-props": [ - "error", - "consistent-as-needed" - ], - "quotes": [ - "error", - "single", - { - "avoidEscape": true, - "allowTemplateLiterals": true - } - ], - "lines-between-class-members": [ - "error", - "always", - { - "exceptAfterSingleLine": true - } - ], - "no-restricted-syntax": [ - "error", - "ForInStatement", - "LabeledStatement", - "WithStatement" - ], - "import/no-unresolved": [ - "error", - { - "ignore": [ - "^photo-sphere-viewer$", - "^photo-sphere-viewer\/dist\/plugins\/([a-z-]+)$" - ] - } - ] - } -} diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 375b5d929..000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,8 +0,0 @@ -**Issues guidelines** - -- Speak English or French. -- Indicate your Photo-Sphere-Viewer, three.js and web browser versions. -- Search in the [documentation](http://photo-sphere-viewer.js.org) before asking. -- Help requests must be exhaustive, precise and come with some code explaining the need (use Markdown code highlight). -- Bug reports must come with a simple test case, preferably on jsFiddle, Plunker, etc. -- Any issue without enough details won't get any answer and will be closed. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..9778787b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,52 @@ +name: '🐞 Bug report' +description: File a report to help us improve +labels: ['bug'] +body: + - type: markdown + attributes: + value: | + ## Thanks for taking the time to fill out this bug report + + **Please use English or French**, I don't speak other languages. + + Use [markdown codeblocks](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) when posting code samples. + + You can quickly create a demo by forking the [basic demo](https://photo-sphere-viewer.js.org/demos/basic/1-zero-config.html) to jsFiddle, Codepen or CodeSandbox. + + - type: textarea + attributes: + label: Describe the bug + placeholder: Provide a clear description of the bug, with tests cases and steps to reproduce. + validations: + required: true + + - type: input + attributes: + label: Online demo URL + + - type: input + attributes: + label: Photo Sphere Viewer version + validations: + required: true + + - type: input + attributes: + label: Plugins loaded + + - type: input + attributes: + label: three.js version + validations: + required: true + + - type: input + attributes: + label: OS & browser + validations: + required: true + + - type: textarea + attributes: + label: Additional context + placeholder: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..3b2c910c4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: '💬 Share something' + url: https://github.com/mistic100/Photo-Sphere-Viewer/discussions/new + about: General discussion outside bugs, features and support diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..3d4e671a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,31 @@ +name: '🚀 Feature request' +description: Suggest an idea for this project +labels: ['feature'] +body: + - type: markdown + attributes: + value: | + ## Thanks for your interest on this project + + **Please use English or French**, I don't speak other languages. + + Consider searching [the documentation](https://photo-sphere-viewer.js.org/guide/) to verify the feature does not already exists. + + Use [markdown codeblocks](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) when posting code samples. + + - type: textarea + attributes: + label: Describe the feature + placeholder: A clear and complete description of the feature you'd like to see in Photo Sphere Viewer. + validations: + required: true + + - type: textarea + attributes: + label: Alternatives you've considered + placeholder: A description of any alternative solutions or features you've considered. If possible provided an online demo of your progression. + + - type: textarea + attributes: + label: Additional context + placeholder: Add any other context about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/support_request.yml b/.github/ISSUE_TEMPLATE/support_request.yml new file mode 100644 index 000000000..9e242e187 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support_request.yml @@ -0,0 +1,40 @@ +name: '🆘 Support request' +description: If you need help to use this library +labels: ['question'] +body: + - type: markdown + attributes: + value: | + ## Let's try to solve your problem + + **Please use English or French**, I don't speak other languages. + + Consider searching [the documentation](https://photo-sphere-viewer.js.org/guide/) to find the solution by yourself. + + Use [markdown codeblocks](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) when posting code samples. + + - type: textarea + attributes: + label: Describe your problem + placeholder: A clear description of what you want to achieve and what you already tried. If possible provided an online demo of your progression. + validations: + required: true + + - type: input + attributes: + label: Online demo URL + + - type: input + attributes: + label: Photo Sphere Viewer version + validations: + required: true + + - type: input + attributes: + label: Plugins loaded + + - type: textarea + attributes: + label: Additional context + placeholder: Add any other context about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 89ee2b825..199d317fb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,5 @@ **Merge request checklist** -- [ ] I created my branch from `dev` and I am issuing the PR to `dev`. -- [ ] All tests pass. If needed, new unit tests were added (only for utils). -- [ ] If needed, the [types](https://github.com/mistic100/Photo-Sphere-Viewer/tree/dev/types) have been updated. -- [ ] If needed, the [documentation](https://github.com/mistic100/Photo-Sphere-Viewer/tree/dev/docs) has been updated. +- [ ] I created my branch from `main` and I am issuing the PR to `main`. +- [ ] All lints and tests pass. If needed, new unit tests were added. +- [ ] If needed, the [documentation](https://github.com/mistic100/Photo-Sphere-Viewer/tree/main/docs) has been updated. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 241f7cff2..e1d021b8c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,8 +1,11 @@ version: 2 updates: - package-ecosystem: npm - directory: "/" + directory: '/' schedule: interval: daily assignees: - mistic100 + ignore: + - dependency-name: '*' + update-types: ['version-update:semver-patch'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..142ab7962 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,39 @@ +name: build + +on: + push: + branches: + - '*' + paths-ignore: + - 'docs/**' + - 'examples/**' + pull_request: + branches: + - main + workflow_dispatch: {} + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'yarn' + + - name: turbo cache + uses: actions/cache@v3 + if: github.ref_name == 'main' + with: + path: .turbo + key: turbo-${{ github.sha }} + restore-keys: | + turbo- + + - name: build + run: | + yarn install + yarn ci:build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 039663894..000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: CI - -on: - push: - branches-ignore: - - master - pull_request: - branches: - - dev - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: '16' - - name: build - run: | - yarn - yarn compile - yarn test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 2ffa5e63d..000000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: publish - -on: - push: - branches: - - master - -jobs: - publish: - if: github.repository == 'mistic100/Photo-Sphere-Viewer' - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - uses: pascalgn/npm-publish-action@master - with: - tag_name: "%s" - tag_message: "%s" - commit_pattern: "^Release (.+)" - workspace: "." - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..df466ade6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: release +run-name: Release ${{ github.ref_name }} + +on: + push: + tags: + - '*' + +jobs: + release: + if: github.repository == 'mistic100/Photo-Sphere-Viewer' + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: '16' + cache: 'yarn' + + - name: build + run: | + yarn install --frozen-lockfile + yarn ci:version ${{ github.ref_name }} + yarn ci:build + + - name: package + run: | + echo '📂 prepare' + node ./build/prepare-package.mjs + echo '📦 archive' + (cd dist && zip -r photo-sphere-viewer-${{ github.ref_name }}.zip .) + + - name: create release + uses: softprops/action-gh-release@v1 + with: + draft: true + files: dist/photo-sphere-viewer-${{ github.ref_name }}.zip + + - name: npm tag + id: npm_tag + run: | + echo "NPM_TAG=$(echo ${{ github.ref_name }} | cut -d '-' -f2 -s | cut -d '.' -f1 -s)" >> $GITHUB_OUTPUT + + - name: npm publish + run: | + yarn ci:publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + NPM_TAG: ${{ steps.npm_tag.outputs.NPM_TAG || 'latest' }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale-issues.yml similarity index 92% rename from .github/workflows/stale.yml rename to .github/workflows/stale-issues.yml index 17772d35e..de2d435c5 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale-issues.yml @@ -1,4 +1,4 @@ -name: stale +name: stale-issues on: schedule: @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/stale@v5 + - uses: actions/stale@v6 with: any-of-labels: 'invalid,info requested' stale-issue-label: wontfix diff --git a/.gitignore b/.gitignore index a5a4966ab..6c2ebfcbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,5 @@ node_modules -/.idea -/*.iml -/dist -/public -package-lock.json -yarn.lock yarn-error.log -/.netlify +.turbo +dist +/public \ No newline at end of file diff --git a/.jsdoc.json b/.jsdoc.json deleted file mode 100644 index 2f0d1dd93..000000000 --- a/.jsdoc.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "opts": { - "recurse": true, - "private": false, - "template": "node_modules/@pixi/jsdoc-template", - "readme": "docs/.jsdoc/index.md" - }, - "plugins": [ - "plugins/markdown", - "@pixi/jsdoc-template/plugins/es6-fix", - "docs/.jsdoc/cleanup-exports" - ], - "templates": { - "applicationName": "Photo Sphere Viewer", - "googleAnalytics": "UA-28192323-3", - "meta": { - "title": "Photo Sphere Viewer API Documentation" - }, - "favicon": "docs/.vuepress/public/favicon.png" - } -} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..5b828daad --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +node_modules +.turbo +dist +/public +packages/virtual-tour-plugin/src/models diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..ff66a9ad6 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,14 @@ +{ + "tabWidth": 4, + "singleQuote": true, + "printWidth": 120, + "quoteProps": "consistent", + "overrides": [ + { + "files": ["**/*.yml", "**/*.json"], + "options": { + "tabWidth": 2 + } + } + ] +} diff --git a/.stylelintrc.json b/.stylelintrc.json index df73f8a7c..892424d8f 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,18 +1,13 @@ { - "extends": [ - "stylelint-config-standard-scss" - ], - "plugins": [ - "stylelint-scss" - ], + "extends": ["stylelint-config-standard-scss"], + "plugins": ["stylelint-scss"], "rules": { - "max-empty-lines": 2, - "max-line-length": null, + "indentation": 4, "color-function-notation": "legacy", "alpha-value-notation": "number", - "number-leading-zero": "never", "scss/dollar-variable-empty-line-before": null, "string-quotes": "single", - "selector-class-pattern": null + "selector-class-pattern": null, + "declaration-colon-newline-after": null } } diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md index 24fc95948..2e3080e7e 100644 --- a/CODE-OF-CONDUCT.md +++ b/CODE-OF-CONDUCT.md @@ -17,24 +17,24 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/LICENSE b/LICENSE index b57decfec..53272a9b3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ The MIT License (MIT) -Copyright (c) 2015 Jérémy Heleine -Copyright (c) 2016-2020 Damien Sorel +Copyright (c) 2014-2015 Jérémy Heleine +Copyright (c) 2016-2022 Damien Sorel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a48434323..3383fbe84 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ # Photo Sphere Viewer -[![NPM version](https://img.shields.io/npm/v/photo-sphere-viewer?logo=npm)](https://www.npmjs.com/package/photo-sphere-viewer) -[![NPM Downloads](https://img.shields.io/npm/dm/photo-sphere-viewer?color=f86036&label=npm&logo=npm)](https://www.npmjs.com/package/photo-sphere-viewer) -[![jsDelivr Hits](https://img.shields.io/jsdelivr/npm/hm/photo-sphere-viewer?color=%23f86036&logo=jsdelivr)](https://www.jsdelivr.com/package/npm/photo-sphere-viewer) -[![Build Status](https://img.shields.io/github/workflow/status/mistic100/Photo-Sphere-Viewer/CI?logo=github)](https://github.com/mistic100/Photo-Sphere-Viewer/actions) +[![NPM version](https://img.shields.io/npm/v/@photo-sphere-viewer/core?logo=npm)](https://www.npmjs.com/package/@photo-sphere-viewer/core) +[![NPM Downloads](https://img.shields.io/npm/dm/@photo-sphere-viewer/core?color=f86036&label=npm&logo=npm)](https://www.npmjs.com/package/@photo-sphere-viewer/core) +[![jsDelivr Hits](https://img.shields.io/jsdelivr/npm/hm/@photo-sphere-viewer/core?color=%23f86036&logo=jsdelivr)](https://www.jsdelivr.com/package/npm/@photo-sphere-viewer/core) +[![Build Status](https://img.shields.io/github/actions/workflow/status/mistic100/Photo-Sphere-Viewer/build.yml?branch=main&logo=github)](https://github.com/mistic100/Photo-Sphere-Viewer/actions/workflows/build.yml) [![Netlify Status](https://img.shields.io/netlify/472fe613-7694-4e61-a662-07e3b988afb3?logo=netlify)](https://photo-sphere-viewer.js.org) Photo Sphere Viewer is a JavaScript library that allows you to display 360×180 degrees panoramas on any web page. Forked from [JeremyHeleine/Photo-Sphere-Viewer](https://github.com/JeremyHeleine/Photo-Sphere-Viewer). - ## Documentation -[photo-sphere-viewer.js.org](https://photo-sphere-viewer.js.org) +[photo-sphere-viewer.js.org](https://photo-sphere-viewer.js.org) ## License + This library is available under the MIT license. diff --git a/build/liveserver.mjs b/build/liveserver.mjs new file mode 100644 index 000000000..3bc687c60 --- /dev/null +++ b/build/liveserver.mjs @@ -0,0 +1,27 @@ +import liveServer from 'alive-server'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootDir = path.resolve(__dirname, '..'); + +const EXAMPLES_DIR = 'examples'; +const PACKAGES_DIR = 'packages'; +const DIST_DIR = 'dist'; + +const packages = fs.readdirSync(path.join(rootDir, PACKAGES_DIR)); + +liveServer.start({ + open: true, + root: path.join(rootDir, EXAMPLES_DIR), + watch: [ + path.join(rootDir, EXAMPLES_DIR), + ...packages.map((name) => path.join(rootDir, PACKAGES_DIR, name, DIST_DIR)), + ], + mount: [ + ['/node_modules', path.join(rootDir, 'node_modules')], + ...packages.map((name) => [`/${DIST_DIR}/${name}`, path.join(rootDir, PACKAGES_DIR, name, DIST_DIR)]), + ], +}); diff --git a/build/plugins/esbuild-plugin-assets.js b/build/plugins/esbuild-plugin-assets.js new file mode 100644 index 000000000..f581be04f --- /dev/null +++ b/build/plugins/esbuild-plugin-assets.js @@ -0,0 +1,26 @@ +import { mkdir, writeFile } from 'fs/promises'; +import path from 'path'; + +/** + * + */ +export function assetsPlugin(files) { + return { + name: 'assets', + setup(build) { + build.onEnd((result) => { + const outdir = build.initialOptions.outdir; + + return mkdir(path.resolve(outdir), { recursive: true }).then(() => + Promise.all( + Object.entries(files).map(([filename, content]) => { + const outpath = outdir + '/' + filename; + console.log('ASSET', outpath); + return content.then((content) => writeFile(outpath, content)); + }) + ) + ); + }); + }, + }; +} diff --git a/build/plugins/esbuild-plugin-css-map.js b/build/plugins/esbuild-plugin-css-map.js new file mode 100644 index 000000000..4f22abac9 --- /dev/null +++ b/build/plugins/esbuild-plugin-css-map.js @@ -0,0 +1,24 @@ +/** + * Alters the paths in CSS maps + */ +export function cssMapPlugin() { + return { + name: 'cssMap', + setup(build) { + build.onEnd((result) => { + const mapFile = result.outputFiles.find((f) => f.path.endsWith('index.css.map')); + if (!mapFile) { + return; + } + + const content = JSON.parse(mapFile.text); + content.sources = content.sources.map((src) => { + return src + .replace('../src', 'src') + .replace('../../shared', '../shared'); + }); + mapFile.contents = Buffer.from(JSON.stringify(content)); + }); + }, + }; +} diff --git a/build/plugins/esbuild-plugin-scss-bundle.js b/build/plugins/esbuild-plugin-scss-bundle.js new file mode 100644 index 000000000..1d41f3224 --- /dev/null +++ b/build/plugins/esbuild-plugin-scss-bundle.js @@ -0,0 +1,31 @@ +import { mkdir, writeFile } from 'fs/promises'; +import path from 'path'; +import { Bundler } from 'scss-bundle'; + +/** + * Generates a bundled scss file + */ +export function scssBundlePlugin() { + return { + name: 'scss-bundle', + setup(build) { + const outdir = build.initialOptions.outdir; + const outpath = outdir + '/index.scss'; + + build.onEnd((result) => { + const scssFile = Object.keys(result.metafile.inputs).find((file) => file.endsWith('.scss')); + if (!scssFile) { + return; + } + + console.log('SCSS', outpath); + + const banner = build.initialOptions.banner.css; + + return mkdir(path.resolve(outdir), { recursive: true }) + .then(() => new Bundler(undefined, process.cwd()).bundle(scssFile)) + .then(({ bundledContent }) => writeFile(path.resolve(outpath), banner + '\n\n' + bundledContent)); + }); + }, + }; +} diff --git a/build/plugins/esbuild-plugin-umd.js b/build/plugins/esbuild-plugin-umd.js new file mode 100644 index 000000000..322b956b0 --- /dev/null +++ b/build/plugins/esbuild-plugin-umd.js @@ -0,0 +1,104 @@ +/** + * This wraps the IIFE output with an UMD loader + */ +export function umdPlugin({ pkg, externals }) { + return { + name: 'umd', + setup(build) { + build.onEnd((result) => { + const iifeFile = result.outputFiles.find((f) => f.path.endsWith('index.js')); + const mapFile = result.outputFiles.find((f) => f.path.endsWith('index.js.map')); + if (!iifeFile) { + return; + } + + console.log('UMD', `Wrap ${iifeFile.path}`); + + // shift js.map mappings + mapFile.contents = Buffer.from(shiftMap(iifeFile.text, pkg, mapFile.text)); + + // add UMD wrapper + iifeFile.contents = Buffer.from(wrapUmd(iifeFile.text, pkg, externals)); + }); + }, + }; +} + +function wrapUmd(fileContent, pkg, externals) { + const deps = Object.keys(pkg.dependencies); + if (!deps.includes('three')) { + deps.unshift('three'); + } + if (deps.includes('@photo-sphere-viewer/shared')) { + deps.splice(deps.indexOf('@photo-sphere-viewer/shared'), 1); + } + + const depsCommonJs = deps.map((dep) => `require('${dep}')`).join(', '); + const depsAmd = deps.map((dep) => `'${dep}'`).join(', '); + const depsGlobal = deps.map((dep) => `global.${externals[dep]}`).join(', '); + const depsParams = deps.map((dep) => `${externals[dep].split('.').pop()}`).join(', '); + + const globalParent = 'PhotoSphereViewer'; + const globalName = pkg.psv.globalName; + const globalExport = !globalName.includes('.') + ? `global.${globalName} = {}` + : `(global.${globalParent} = global.${globalParent} || {}, global.${globalName} = {})`; + + return `(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, ${depsCommonJs}) : + typeof define === 'function' && define.amd ? define(['exports', ${depsAmd}], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(${globalExport}, ${depsGlobal})); +})(this, (function (exports, ${depsParams}) { + +${fileContent + // remove iife assignation + .replace(`var ${globalParent};\n`, '') + .replace(`var ${globalParent} = (() => {\n`, '') + .replace(`(${globalParent} ||= {}).${globalName.split('.').pop()} = (() => {\n`, '') + // hydrate exports + .replace(/return __toCommonJS\((.*?)\);\n}\)\(\);/, '__copyProps(__defProp(exports, "__esModule", { value: true }), $1);') + // unused function + .replace(/ var __toCommonJS = (.*?);\n/, '') + + // simplify static fields + .replace(/__publicField\((.*?), "(.*?)", ([\s\S]*?)\);/g, '$1.$2 = $3;') + .replace(/__publicField\((.*?), "(.*?)"\);/g, '$1.$2 = undefined;') + // unused functions + .replace(/ var __publicField = ([\s\S]*?)};\n/, '') + .replace(/ var __defNormalProp = (.*?);\n/, '') + + // simplify imports + .replace( + /__commonJS\({[\s]+"(.*?)"\(exports, module\) {[\s]+module.exports = (.*?);[\s]+}[\s]+}\);/g, + (_, p1, p2) => `() => ${p2.split('.').pop()};` + ) + .replace(/__toESM\((.*?)\(\)\)/g, '$1()') + // unused functions + .replace(/ var __toESM = ([\s\S]*?)\)\);\n/, '') + .replace(/ var __commonJS = ([\s\S]*?)};\n/, '') + .replace(/ var __create = (.*?);\n/, '') + .replace(/ var __getProtoOf = (.*?);\n/, '') + // remove plugin reference + .replace(/external-global-plugin:/g, '')} +}));`; +} + +function shiftMap(fileContent, pkg, mapContent) { + // prettier-ignore + const offset = + 6 /* header */ + - 1 - (pkg.psv.globalName.includes('.') ? 1 : 0) /* iife */ + - 10 - (fileContent.match(/__publicField\(/) ? 5 : 0) /* unused functions */ + - 4 * fileContent.match(/__commonJS\({/g).length; /* imports */ + + const content = JSON.parse(mapContent); + content.sources = content.sources.map((src) => { + return src + .replace('../src', 'src') + .replace('../../shared', '../shared') + .replace('external-global-plugin:', ''); + }); + content.mappings = content.mappings.slice(-offset); + + return JSON.stringify(content); +} diff --git a/build/prepare-package.mjs b/build/prepare-package.mjs new file mode 100644 index 000000000..2e1c98c45 --- /dev/null +++ b/build/prepare-package.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node + +import path from 'path'; +import fs from 'fs-extra'; + +const PACKAGES_DIR = 'packages'; +const DIST_DIR = 'dist'; +const LICENSE_FILE = 'LICENSE'; + +fs.readdirSync(PACKAGES_DIR) + .filter(name => name !== 'shared') + .forEach(name => { + const source = path.join(PACKAGES_DIR, name, DIST_DIR); + const destination = path.join(DIST_DIR, name); + + console.log(`copy ${name}`); + + fs.copySync(source, destination, { + filter(name) { + return name === source || ['js', 'css', 'scss', 'ts', 'map', 'json'].some(ext => name.endsWith(ext)); + } + }); + }); + +console.log(`COPY ${LICENSE_FILE}`); +fs.copySync(LICENSE_FILE, path.join(DIST_DIR, LICENSE_FILE)); diff --git a/build/templates/license.js b/build/templates/license.js new file mode 100644 index 000000000..5f343f9c0 --- /dev/null +++ b/build/templates/license.js @@ -0,0 +1,4 @@ +import { readFile } from 'fs/promises'; +import path from 'path'; + +export const license = () => readFile(path.join(__dirname, '../../LICENSE'), { encoding: 'utf8' }); diff --git a/build/templates/npmrc.js b/build/templates/npmrc.js new file mode 100644 index 000000000..f44e7a5f5 --- /dev/null +++ b/build/templates/npmrc.js @@ -0,0 +1,4 @@ +export const npmrc = () => + Promise.resolve(`@photo-sphere-viewer:registry=https://registry.npmjs.org +//registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN} +`); diff --git a/build/templates/package.js b/build/templates/package.js new file mode 100644 index 000000000..a268900f3 --- /dev/null +++ b/build/templates/package.js @@ -0,0 +1,45 @@ +export const packageJson = (pkg) => + import('sort-package-json').then(({ default: sortPackageJson, sortOrder }) => { + sortOrder.splice(sortOrder.indexOf('style') + 1, 0, 'sass'); + + let content = { + ...pkg, + main: 'index.js', + module: 'index.module.js', + types: 'index.d.ts', + license: 'MIT', + repository: { + type: 'git', + url: 'git://github.com/mistic100/Photo-Sphere-Viewer.git', + }, + author: { + name: "Damien 'Mistic' Sorel", + email: 'contact@git.strangeplanet.fr', + homepage: 'https://www.strangeplanet.fr', + }, + keywords: ['photosphere', 'panorama', 'threejs'], + }; + + if (pkg.psv.style) { + content.style = 'index.css'; + content.sass = 'index.scss'; + } + + if (pkg.name === '@photo-sphere-viewer/core') { + content.contributors = [ + { + name: 'Jérémy Heleine', + email: 'jeremy.heleine@gmail.com', + homepage: 'https://jeremyheleine.me', + }, + ]; + } + + delete content.dependencies['@photo-sphere-viewer/shared']; + delete content.devDependencies; + delete content.psv; + delete content.scripts; + delete content.typedoc; + + return JSON.stringify(sortPackageJson(content), null, 2); + }); diff --git a/build/templates/readme.js b/build/templates/readme.js new file mode 100644 index 000000000..c6f2611f8 --- /dev/null +++ b/build/templates/readme.js @@ -0,0 +1,20 @@ +export const readme = (pkg) => { + const title = pkg.psv.globalName + .replace(/([A-Z])/g, ' $1') + .replace('.', ' /') + .trim(); + + return Promise.resolve(`${title} +----- + +${pkg.description} + +## Documentation + +${pkg.homepage} + +## License + +This library is available under the MIT license. +`); +}; diff --git a/build/tsup.config.js b/build/tsup.config.js new file mode 100644 index 000000000..d0a8a9bd8 --- /dev/null +++ b/build/tsup.config.js @@ -0,0 +1,74 @@ +import { externalGlobalPlugin } from 'esbuild-plugin-external-global'; +import { sassPlugin } from 'esbuild-sass-plugin'; +import { defineConfig } from 'tsup'; +import { assetsPlugin } from './plugins/esbuild-plugin-assets'; +import { scssBundlePlugin } from './plugins/esbuild-plugin-scss-bundle'; +import { umdPlugin } from './plugins/esbuild-plugin-umd'; +import { cssMapPlugin } from './plugins/esbuild-plugin-css-map'; +import { license } from './templates/license'; +import { npmrc } from './templates/npmrc'; +import { packageJson } from './templates/package'; +import { readme } from './templates/readme'; + +const externals = { + 'three': 'THREE', + '@photo-sphere-viewer/core': 'PhotoSphereViewer', + '@photo-sphere-viewer/cubemap-adapter': 'PhotoSphereViewer.CubemapAdapter', + '@photo-sphere-viewer/gyroscope-plugin': 'PhotoSphereViewer.GyroscopePlugin', + '@photo-sphere-viewer/settings-plugin': 'PhotoSphereViewer.SettingsPlugin', +}; + +export default function createConfig(pkg) { + const banner = `/*! + * ${pkg.psv.globalName} ${pkg.version} +${ + pkg.name === '@photo-sphere-viewer/core' ? ' * @copyright 2014-2015 Jérémy Heleine\n' : '' +} * @copyright ${new Date().getFullYear()} Damien "Mistic" Sorel + * @licence MIT (https://opensource.org/licenses/MIT) + */`; + + return defineConfig((options) => { + const dev = options.watch || options.define?.['config'] === 'dev'; + return { + entryPoints: ['src/index.ts'], + outDir: 'dist', + format: dev ? ['iife'] : ['iife', 'esm'], + globalName: pkg.psv.globalName, + outExtension({ format }) { + return { + js: format === 'iife' ? '.js' : '.module.js', + }; + }, + dts: !dev, + sourcemap: true, + external: Object.keys(externals), + esbuildPlugins: [ + sassPlugin(), + externalGlobalPlugin(externals), + umdPlugin({ pkg, externals }), + cssMapPlugin(), + ...(dev + ? [] + : [ + scssBundlePlugin(), + assetsPlugin({ + 'LICENSE': license(), + '.npmrc': npmrc(), + 'README.md': readme(pkg), + 'package.json': packageJson(pkg), + }), + ]), + ], + esbuildOptions(options, context) { + options.banner = { + js: banner, + css: banner, + }; + options.loader = { + '.svg': 'text', + }; + }, + clean: true, + }; + }); +} diff --git a/docs/.jsdoc/cleanup-exports.js b/docs/.jsdoc/cleanup-exports.js deleted file mode 100644 index 9364a777a..000000000 --- a/docs/.jsdoc/cleanup-exports.js +++ /dev/null @@ -1,10 +0,0 @@ -exports.handlers = { - newDoclet: function (e) { - e.doclet.name = e.doclet.name.replace('exports.', ''); - e.doclet.longname = e.doclet.longname.replace('exports.', ''); - - if (typeof e.doclet.meta.code.name === 'string') { - e.doclet.meta.code.name = e.doclet.meta.code.name.replace('exports.', ''); - } - }, -}; diff --git a/docs/.jsdoc/index.md b/docs/.jsdoc/index.md deleted file mode 100644 index b203acc4e..000000000 --- a/docs/.jsdoc/index.md +++ /dev/null @@ -1,27 +0,0 @@ -# Photo Sphere Viewer API Documentation - -[← Main documentation](..) - ---- - -## Most used - -- [Viewer](PSV.Viewer.html) -- [Events](PSV.html#.event:autorotate) -- [Plugins](PSV.plugins.html) -- [Adapters](PSV.adapters.html) - ---- - -## Exported members - -- [AbstractAdapter](PSV.adapters.AbstractAdapter.html) - Base class for render adapters -- [AbstractButton](PSV.buttons.AbstractButton.html) - Base class for buttons -- [AbstractPlugin](PSV.plugins.AbstractPlugin.html) - Base class for plugins -- [CONSTANTS](PSV.constants.html) - All internal constants -- [DEFAULTS](PSV.html#.DEFAULTS) - Default configuration -- [PSVError](PSV.PSVError.html) - Generic error -- [registerButton](PSV.html#.registerButton) - Helper for registering buttons -- [SYSTEM](PSV.html#.SYSTEM) - System informations -- [utils](PSV.utils.html) - Various utilities -- [Viewer](PSV.Viewer.html) - Base class diff --git a/docs/.netlify/README.md b/docs/.netlify/README.md new file mode 100644 index 000000000..1c5f0dc09 --- /dev/null +++ b/docs/.netlify/README.md @@ -0,0 +1,9 @@ +# Netlify config for PSV doc + +## Env variables + +- AWS_LAMBDA_JS_RUNTIME: nodejs18.x +- GH_TOKEN: xxxxxx +- NETLIFY_USE_YARN: true +- NODE_OPTIONS: --max-old-space-size=4096 +- NODE_VERSION: 16 diff --git a/docs/.netlify/functions/announcements.js b/docs/.netlify/functions/announcements.js index 08f175c45..6956b5c48 100644 --- a/docs/.netlify/functions/announcements.js +++ b/docs/.netlify/functions/announcements.js @@ -1,15 +1,15 @@ -exports.handler = async function(event, context) { - if (event.httpMethod !== 'GET') { - return { statusCode: 405, body: 'Method Not Allowed' }; - } +exports.handler = async function (event, context) { + if (event.httpMethod !== 'GET') { + return { statusCode: 405, body: 'Method Not Allowed' }; + } - return await fetch('https://api.github.com/graphql', { - method : 'POST', - headers: { - 'Authorization': `bearer ${process.env.GH_TOKEN}`, - }, - body : JSON.stringify({ - query: ` + return await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + Authorization: `bearer ${process.env.GH_TOKEN}`, + }, + body: JSON.stringify({ + query: ` query { repository(owner: "mistic100", name: "photo-sphere-viewer") { pinnedDiscussions(first: 2) { @@ -24,14 +24,14 @@ exports.handler = async function(event, context) { } } }`, - }), - }) - .then(response => response.json()) - .then(result => { - const announcements = result.data.repository.pinnedDiscussions.nodes.map(n => n.discussion); - return { - statusCode: 200, - body: JSON.stringify(announcements) - }; - }); + }), + }) + .then((response) => response.json()) + .then((result) => { + const announcements = result.data.repository.pinnedDiscussions.nodes.map((n) => n.discussion); + return { + statusCode: 200, + body: JSON.stringify(announcements), + }; + }); }; diff --git a/docs/.netlify/functions/releases.js b/docs/.netlify/functions/releases.js index ed01a12d1..dd9e7654f 100644 --- a/docs/.netlify/functions/releases.js +++ b/docs/.netlify/functions/releases.js @@ -1,15 +1,15 @@ -exports.handler = async function(event, context) { - if (event.httpMethod !== 'GET') { - return { statusCode: 405, body: 'Method Not Allowed' }; - } +exports.handler = async function (event, context) { + if (event.httpMethod !== 'GET') { + return { statusCode: 405, body: 'Method Not Allowed' }; + } - return await fetch('https://api.github.com/graphql', { - method : 'POST', - headers: { - 'Authorization': `bearer ${process.env.GH_TOKEN}`, - }, - body : JSON.stringify({ - query: ` + return await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + Authorization: `bearer ${process.env.GH_TOKEN}`, + }, + body: JSON.stringify({ + query: ` query { repository(owner: "mistic100", name: "photo-sphere-viewer") { releases(first: 20, orderBy: {field: CREATED_AT, direction: DESC}) { @@ -23,14 +23,14 @@ exports.handler = async function(event, context) { } } }`, - }), - }) - .then(response => response.json()) - .then(result => { - const releases = result.data.repository.releases.nodes; - return { - statusCode: 200, - body: JSON.stringify(releases) - }; - }); + }), + }) + .then((response) => response.json()) + .then((result) => { + const releases = result.data.repository.releases.nodes; + return { + statusCode: 200, + body: JSON.stringify(releases), + }; + }); }; diff --git a/docs/.typedoc/style.css b/docs/.typedoc/style.css new file mode 100644 index 000000000..e973dea51 --- /dev/null +++ b/docs/.typedoc/style.css @@ -0,0 +1,36 @@ +.tsd-kind-module > ul { + display: none; +} + +.tsd-parameter { + border: 1px solid var(--color-accent); + list-style: none; +} + +.tsd-parameter > h5 { + margin: 0; + padding: 10px; + background: var(--color-background-secondary); +} + +.tsd-parameter:not(:first-child) { + border-top: none; +} + +.tsd-parameter .tsd-comment { + padding: 10px; +} + +.tsd-parameter .tsd-comment h3 { + font-size: 1rem; + margin-right: 0.5rem; + display: inline-block; +} + +.tsd-parameter .tsd-comment h3:after { + content: ': '; +} + +.tsd-parameter .tsd-comment h3 + p { + display: inline-block; +} diff --git a/docs/.vuepress/components/Announcements.vue b/docs/.vuepress/components/Announcements.vue index f655ef72c..69ff72620 100644 --- a/docs/.vuepress/components/Announcements.vue +++ b/docs/.vuepress/components/Announcements.vue @@ -1,75 +1,75 @@ diff --git a/docs/.vuepress/components/ApiButton.vue b/docs/.vuepress/components/ApiButton.vue deleted file mode 100644 index d58bef655..000000000 --- a/docs/.vuepress/components/ApiButton.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/docs/.vuepress/components/ApiLink.vue b/docs/.vuepress/components/ApiLink.vue index 4db9dac4d..7b737acdf 100644 --- a/docs/.vuepress/components/ApiLink.vue +++ b/docs/.vuepress/components/ApiLink.vue @@ -1,17 +1,25 @@ diff --git a/docs/.vuepress/components/Changelog.vue b/docs/.vuepress/components/Changelog.vue index 831eceb75..7f1097d40 100644 --- a/docs/.vuepress/components/Changelog.vue +++ b/docs/.vuepress/components/Changelog.vue @@ -1,129 +1,137 @@ diff --git a/docs/.vuepress/components/CropPlayground.vue b/docs/.vuepress/components/CropPlayground.vue index b84004c36..c87965a61 100644 --- a/docs/.vuepress/components/CropPlayground.vue +++ b/docs/.vuepress/components/CropPlayground.vue @@ -1,178 +1,205 @@ diff --git a/docs/.vuepress/components/Playground.vue b/docs/.vuepress/components/Playground.vue index b73f0178f..c3a899637 100644 --- a/docs/.vuepress/components/Playground.vue +++ b/docs/.vuepress/components/Playground.vue @@ -1,1086 +1,1193 @@ diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 3389945f7..c39744ceb 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -2,147 +2,157 @@ const path = require('path'); const fs = require('fs'); function posixJoin(...args) { - return path.join(...args).split(path.sep).join(path.posix.sep); // Windows compat... + return path + .join(...args) + .split(path.sep) + .join(path.posix.sep); // Windows compat... } function listFiles(dir) { - const dirents = fs.readdirSync(dir, { withFileTypes: true }); - const files = dirents.map((dirent) => { - const res = posixJoin(dir, dirent.name); - return dirent.isDirectory() ? listFiles(res) : res; - }); - return files.flat(); + const dirents = fs.readdirSync(dir, { withFileTypes: true }); + const files = dirents.map((dirent) => { + const res = posixJoin(dir, dirent.name); + return dirent.isDirectory() ? listFiles(res) : res; + }); + return files.flat(); } function getFiles(dir) { - const abolsuteDir = posixJoin(process.cwd(), dir); - return listFiles(abolsuteDir).map(f => f.substr(abolsuteDir.length + 1)) + const absoluteDir = posixJoin(process.cwd(), dir); + return listFiles(absoluteDir).map((f) => f.substr(absoluteDir.length + 1)); } - module.exports = { - dest : './public', - title : 'Photo Sphere Viewer', - description: 'A JavaScript library to display Photo Sphere panoramas', - head : [ - ['link', { rel: 'icon', href: '/favicon.png' }], - ['script', { src: 'https://cdn.jsdelivr.net/npm/uevent@2/browser.js', defer: 'defer' }], - ['script', { src: 'https://cdn.jsdelivr.net/npm/three/build/three.min.js', defer: 'defer' }], - ['script', { src : 'https://cdn.jsdelivr.net/npm/photo-sphere-viewer@4/dist/photo-sphere-viewer.js', defer: 'defer' }], - ['link', { rel : 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/photo-sphere-viewer@4/dist/photo-sphere-viewer.css' }], - ], - themeConfig: { - logo : '/favicon.png', - repo : 'mistic100/Photo-Sphere-Viewer', - docsDir : 'docs', - docsBranch : 'dev', - editLinks : true, - smoothScroll: true, - sidebarDepth: 3, - algolia : { - appId : '5AVMW192FM', - apiKey : 'd443b6c08ed5353575f503b7a57f5bbf', - indexName: 'photo-sphere-viewer', - }, - nav : [ - { text: 'Guide', link: '/guide/' }, - { text: 'Plugins', link: '/plugins/' }, - { text: 'Playground', link: '/playground' }, - { text: 'Demos', link: '/demos/' }, - { text: 'API', link: 'https://photo-sphere-viewer.js.org/api/' }, - { text: 'Changelog', link: '/changelog' }, + dest: './public', + title: 'Photo Sphere Viewer', + description: 'A JavaScript library to display Photo Sphere panoramas', + // prettier-ignore + head: [ + ['link', { rel: 'icon', href: '/favicon.png' }], + ['script', { src: 'https://cdn.jsdelivr.net/npm/three/build/three.min.js', defer: 'defer' }], + ['script', { src: 'https://cdn.jsdelivr.net/npm/@photo-sphere-viewer/core@alpha/index.js', defer: 'defer' }], + ['link', { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/@photo-sphere-viewer/core@alpha/index.css' }], ], - sidebar : { - '/guide/' : [ - { - title : 'Guide', - sidebarDepth: 3, - collapsable : false, - children : [ - '', - 'config', - 'methods', - 'events', - 'navbar', - 'style', - { - title : 'Adapters', - path : '/guide/adapters/', - collapsable: true, - children : [ - 'adapters/equirectangular', - 'adapters/equirectangular-tiles', - 'adapters/equirectangular-video', - 'adapters/cubemap', - 'adapters/cubemap-tiles', - 'adapters/cubemap-video', - 'adapters/little-planet', - ], - }, + themeConfig: { + logo: '/favicon.png', + repo: 'mistic100/Photo-Sphere-Viewer', + docsDir: 'docs', + docsBranch: 'main', + editLinks: true, + smoothScroll: true, + sidebarDepth: 3, + algolia: { + appId: '5AVMW192FM', + apiKey: 'd443b6c08ed5353575f503b7a57f5bbf', + indexName: 'photo-sphere-viewer', + }, + nav: [ + { text: 'Guide', link: '/guide/' }, + { text: 'Plugins', link: '/plugins/' }, + { text: 'Playground', link: '/playground' }, + { text: 'Demos', link: '/demos/' }, + { text: 'API', link: 'https://photo-sphere-viewer.js.org/api/' }, { - title : 'Reusable components', - path : '/guide/components/', - collapsable: true, - children : [ - 'components/panel', - 'components/notification', - 'components/overlay', - 'components/tooltip', - ], + text: 'v5', + ariaLavel: 'version', + items: [ + { text: 'v4', link: 'https://photo-sphere-viewer-4.netlify.app' }, + { text: 'v3', link: 'https://photo-sphere-viewer-3.netlify.app' }, + ], }, - 'frameworks', - ], - }, - ], - '/plugins/': [ - { - title : 'Plugins', - collapsable : false, - children : [ - '', - 'writing-a-plugin', - ], - }, - { - title : 'Official plugins', - collapsable : false, - children : getFiles('docs/plugins') - .filter(f => f.indexOf('plugin-') === 0), - }, - ], - '/demos/': [ - { - title : 'Demos', - path : '/demos/', - sidebarDepth: 0, - collapsable : false, - children : (() => { - const demoFiles = getFiles('docs/demos') - .map(f => f.split('/')) - .filter(f => f.length === 2) - .reduce((groups, [dir, file]) => { - (groups[dir] = groups[dir] ?? []).push(file); - return groups; - }, {}); + { text: 'Changelog', link: '/changelog' }, + ], + sidebar: { + '/guide/': [ + { + title: 'Guide', + sidebarDepth: 3, + collapsable: false, + children: [ + '', + 'config', + 'methods', + 'events', + 'navbar', + 'style', + { + title: 'Adapters', + path: '/guide/adapters/', + collapsable: true, + children: [ + 'adapters/equirectangular', + 'adapters/equirectangular-tiles', + 'adapters/equirectangular-video', + 'adapters/cubemap', + 'adapters/cubemap-tiles', + 'adapters/cubemap-video', + 'adapters/little-planet', + ], + }, + { + title: 'Reusable components', + path: '/guide/components/', + collapsable: true, + children: [ + 'components/panel', + 'components/notification', + 'components/overlay', + 'components/tooltip', + ], + }, + 'frameworks', + 'migration', + ], + }, + ], + '/plugins/': [ + { + title: 'Plugins', + collapsable: false, + children: ['', 'writing-a-plugin'], + }, + { + title: 'Official plugins', + collapsable: false, + children: getFiles('docs/plugins').filter((f) => f !== 'README.md' && f !== 'writing-a-plugin.md'), + }, + ], + '/demos/': [ + { + title: 'Demos', + path: '/demos/', + sidebarDepth: 0, + collapsable: false, + children: (() => { + const demoFiles = getFiles('docs/demos') + .map((f) => f.split('/')) + .filter((f) => f.length === 2) + .reduce((groups, [dir, file]) => { + (groups[dir] = groups[dir] ?? []).push(file); + return groups; + }, {}); - return Object.entries(demoFiles) - .map(([group, files]) => ({ - title : group[0].toUpperCase() + group.substr(1), - collapsable: false, - children : files.map(f => `${group}/${f}`), - })); - })(), + return Object.entries(demoFiles).map(([group, files]) => ({ + title: group[0].toUpperCase() + group.substring(1), + collapsable: false, + children: files.map((f) => `${group}/${f}`), + })); + })(), + }, + ], }, - ], }, - }, - plugins : [ - ['@vuepress/google-analytics', { - 'ga': 'UA-28192323-3', - }], - ['@vuepress/back-to-top'], - require('./plugins/gallery'), - require('./plugins/code-demo'), - require('./plugins/tabs'), - ], + plugins: [ + [ + '@vuepress/google-analytics', + { + ga: 'UA-28192323-3', + }, + ], + ['@vuepress/back-to-top'], + require('./plugins/gallery'), + require('./plugins/code-demo'), + require('./plugins/module'), + require('./plugins/tabs'), + ], }; diff --git a/docs/.vuepress/enhanceApp.js b/docs/.vuepress/enhanceApp.js index 80ccf3cdb..70f199a23 100644 --- a/docs/.vuepress/enhanceApp.js +++ b/docs/.vuepress/enhanceApp.js @@ -3,24 +3,24 @@ import VSwatches from 'vue-swatches'; import VueSlider from 'vue-slider-component/dist-css/vue-slider-component.umd.min'; import NoSSR from 'vue-no-ssr'; -import 'vue-material/dist/theme/default.css' -import 'vue-material/dist/vue-material.min.css' +import 'vue-material/dist/theme/default.css'; +import 'vue-material/dist/vue-material.min.css'; import 'vue-slider-component/dist-css/vue-slider-component.css'; -import 'vue-slider-component/theme/material.css' +import 'vue-slider-component/theme/material.css'; import 'vue-swatches/dist/vue-swatches.css'; export default ({ Vue, router }) => { - Vue.use(VueMaterial); - Vue.component('v-swatches', VSwatches); - Vue.component('vue-slider', VueSlider); - Vue.component('no-ssr', NoSSR); + Vue.use(VueMaterial); + Vue.component('v-swatches', VSwatches); + Vue.component('vue-slider', VueSlider); + Vue.component('no-ssr', NoSSR); - router.beforeEach((to, from, next) => { - if (/^\/api/.test(to.fullPath)) { - window.location.href = `${to.path}.html${to.hash}`; - next(false); - } else { - next(); - } - }); + router.beforeEach((to, from, next) => { + if (/^\/api/.test(to.fullPath)) { + window.location.href = `${to.path}.html${to.hash}`; + next(false); + } else { + next(); + } + }); }; diff --git a/docs/.vuepress/plugins/code-demo/CodeDemo.vue b/docs/.vuepress/plugins/code-demo/CodeDemo.vue index 3980b8430..2f114bc91 100644 --- a/docs/.vuepress/plugins/code-demo/CodeDemo.vue +++ b/docs/.vuepress/plugins/code-demo/CodeDemo.vue @@ -1,109 +1,114 @@ diff --git a/docs/.vuepress/plugins/code-demo/ServiceButton.vue b/docs/.vuepress/plugins/code-demo/ServiceButton.vue index d80a6474d..088bc42e1 100644 --- a/docs/.vuepress/plugins/code-demo/ServiceButton.vue +++ b/docs/.vuepress/plugins/code-demo/ServiceButton.vue @@ -1,91 +1,92 @@ diff --git a/docs/.vuepress/plugins/code-demo/constants.js b/docs/.vuepress/plugins/code-demo/constants.js index 70617eadd..63de4f6ec 100644 --- a/docs/.vuepress/plugins/code-demo/constants.js +++ b/docs/.vuepress/plugins/code-demo/constants.js @@ -1,30 +1,30 @@ import ICON_CODEPEN from '!raw-loader!./icons/codepen.svg'; -// import ICON_CODESANDBOX from '!raw-loader!./icons/codesandbox.svg'; +import ICON_CODESANDBOX from '!raw-loader!./icons/codesandbox.svg'; import ICON_JSFIDDLE from '!raw-loader!./icons/jsfiddle.svg'; export const SERVICES = [ - // https://blog.codepen.io/documentation/api/prefill - 'codepen', - // https://docs.jsfiddle.net/api/display-a-fiddle-from-pos - 'jsfiddle', - // https://codesandbox.io/docs/importing#define-api - // 'codesandbox', + // https://blog.codepen.io/documentation/api/prefill + 'codepen', + // https://docs.jsfiddle.net/api/display-a-fiddle-from-pos + 'jsfiddle', + // https://codesandbox.io/docs/importing#define-api + 'codesandbox', ]; export const SERVICE_URL = { - codepen : 'https://codepen.io/pen/define', - jsfiddle : 'https://jsfiddle.net/api/post/library/pure', - // codesandbox: 'https://codesandbox.io/api/v1/sandboxes/define', + codepen: 'https://codepen.io/pen/define', + jsfiddle: 'https://jsfiddle.net/api/post/library/pure', + codesandbox: 'https://codesandbox.io/api/v1/sandboxes/define', }; export const SERVICE_NAME = { - codepen : 'Codepen', - jsfiddle : 'JSFiddle', - // codesandbox: 'CodeSandbox', + codepen: 'Codepen', + jsfiddle: 'JSFiddle', + codesandbox: 'CodeSandbox', }; export const SERVICE_ICON = { - codepen : ICON_CODEPEN, - jsfiddle : ICON_JSFIDDLE, - // codesandbox: ICON_CODESANDBOX, + codepen: ICON_CODEPEN, + jsfiddle: ICON_JSFIDDLE, + codesandbox: ICON_CODESANDBOX, }; diff --git a/docs/.vuepress/plugins/code-demo/enhanceApp.js b/docs/.vuepress/plugins/code-demo/enhanceApp.js index 20efdc052..a81e562a6 100644 --- a/docs/.vuepress/plugins/code-demo/enhanceApp.js +++ b/docs/.vuepress/plugins/code-demo/enhanceApp.js @@ -2,6 +2,6 @@ import CodeDemo from './CodeDemo.vue'; import ServiceButton from './ServiceButton.vue'; export default ({ Vue }) => { - Vue.component('CodeDemo', CodeDemo); - Vue.component('ServiceButton', ServiceButton); + Vue.component('CodeDemo', CodeDemo); + Vue.component('ServiceButton', ServiceButton); }; diff --git a/docs/.vuepress/plugins/code-demo/index.js b/docs/.vuepress/plugins/code-demo/index.js index 543410be1..932666513 100644 --- a/docs/.vuepress/plugins/code-demo/index.js +++ b/docs/.vuepress/plugins/code-demo/index.js @@ -5,48 +5,47 @@ const { parse: parseYaml } = require('yaml'); const BLOCK_NAME = 'code-demo'; module.exports = (options, ctx) => ({ - name : 'code-demo', - enhanceAppFiles: path.resolve(__dirname, './enhanceApp.js'), - extendMarkdown : (md) => { - md.use(container, BLOCK_NAME, { - render: (tokens, idx) => { - const { nesting } = tokens[idx]; + name: 'code-demo', + enhanceAppFiles: path.resolve(__dirname, './enhanceApp.js'), + extendMarkdown: (md) => { + md.use(container, BLOCK_NAME, { + render: (tokens, idx) => { + const { nesting } = tokens[idx]; - if (nesting === 1) { - const config = { - title: '', - html: '', - js : '', - css : '', - resources: [], - }; - for (let index = idx; index < tokens.length; index++) { - const { type, content, info: info } = tokens[index]; - if (type === `container_${BLOCK_NAME}_close`) { - break; - } - if (type === 'fence') { - if (info === 'yaml' || info === 'yml') { - const { title, resources } = parseYaml(content); - config.title = title; - config.resources = resources || []; - } else { - config[info] = content; - } - } - } + if (nesting === 1) { + const config = { + title: '', + html: '', + js: '', + css: '', + packages: [], + }; + for (let index = idx; index < tokens.length; index++) { + const { type, content, info: info } = tokens[index]; + if (type === `container_${BLOCK_NAME}_close`) { + break; + } + if (type === 'fence') { + if (info === 'yaml' || info === 'yml') { + const { title, packages } = parseYaml(content); + config.title = title; + config.packages = packages || []; + } else { + config[info] = content; + } + } + } - return `\n`; - } - }, - }); - } + } else { + return `\n`; + } + }, + }); + }, }); diff --git a/docs/.vuepress/plugins/code-demo/utils.js b/docs/.vuepress/plugins/code-demo/utils.js index e6a8aa1be..9ab156964 100644 --- a/docs/.vuepress/plugins/code-demo/utils.js +++ b/docs/.vuepress/plugins/code-demo/utils.js @@ -1,13 +1,24 @@ -// import { getParameters } from 'codesandbox/lib/api/define'; +import { getParameters } from 'codesandbox-import-utils/lib/api/define'; -const BASE_URL = 'https://cdn.jsdelivr.net/npm/photo-sphere-viewer@4/dist/'; +const ORG = '@photo-sphere-viewer/'; +const CDN_BASE = 'https://cdn.jsdelivr.net/npm/'; +const VERSION = 'alpha'; +const THREE_PATH = CDN_BASE + 'three/build/three.min.js'; + +function fullname(name) { + return ORG + name; +} + +function buildPath(name, type) { + return CDN_BASE + name + '@' + VERSION + '/index.' + type; +} export function getFullJs(js) { - return js; + return js; } export function getFullCss(css) { - return ` + return ` html, body, #viewer { margin: 0; width: 100vw; @@ -15,34 +26,40 @@ html, body, #viewer { font-family: sans-serif; } -${css} -`; +${css}`.trim(); } export function getFullHtml(html) { - return ` + return `
-${html} -`; +${html}`.trim(); +} + +export function getFullPackages(packages) { + return [ + { + name: fullname('core'), + imports: 'Viewer', + style: true, + }, + ...packages.map((pkg) => ({ + ...pkg, + name: fullname(pkg.name), + })), + ]; } -export function getFullResources(resources) { - return [ - { path: 'https://cdn.jsdelivr.net/npm/uevent@2/browser.js', type: 'js' }, - { path: 'https://cdn.jsdelivr.net/npm/three/build/three.min.js', type: 'js' }, - { path: BASE_URL + 'photo-sphere-viewer.js', type: 'js', imports: ['Viewer'] }, - { path: BASE_URL + 'photo-sphere-viewer.css', type: 'css' }, - ...resources.map(({ path, imports }) => ({ - path : BASE_URL + path, - imports: imports?.split(' '), - type : path.match(/\.js$/) ? 'js' : 'css', - })), - ]; +export function getAllResources(packages) { + return [ + THREE_PATH, + ...packages.map(({ name }) => buildPath(name, 'js')), + ...packages.filter(({ style }) => style).map(({ name }) => buildPath(name, 'css')), + ]; } -export function getIframeContent({ title, html, js, css, resources }) { - return ` +export function getIframeContent({ title, html, js, css, packages }) { + return ` @@ -51,70 +68,104 @@ export function getIframeContent({ title, html, js, css, resources }) { ${title} -${resources - .map(({ path, type }) => { - if (type === 'js') { - return ` + - - -`; +`; } -export function getJsFiddleResources(resources) { - return resources.map(({ path }) => path); +export function getCodePenValue({ title, js, css, html, packages }) { + const resources = getAllResources(packages); + + return JSON.stringify({ + title: title, + js: js, + css: css, + html: html, + js_external: resources.filter((path) => path.endsWith('.js')), + css_external: resources.filter((path) => path.endsWith('.css')), + }); } -export function getCodePenValue({ title, js, css, html, resources }) { - return JSON.stringify({ - title : title, - js : js, - css : css, - html : html, - js_external : resources.filter(({ type }) => type === 'js').map(({ path }) => path), - css_external: resources.filter(({ type }) => type === 'css').map(({ path }) => path), - }); -} +export function getCodeSandboxValue({ title, js, css, html, packages }) { + return getParameters({ + files: { + 'package.json': { + content: { + description: title, + main: 'index.html', + scripts: { + start: 'parcel index.html --open', + build: 'parcel build index.html', + }, + dependencies: packages.reduce((deps, { name }) => { + deps[name] = VERSION; + return deps; + }, {}), + devDependencies: { + 'parcel-bundler': '^2.8.0', + 'typescript': '^4.8', + }, + }, + }, + 'index.html': { + isBinary: false, + content: ` + + + + + + ${title} + + + + ${html} -// FIXME : proper config -// export function getCodeSandboxValue({ title, js, css, html, resources }) { -// return getParameters({ -// files: { -// 'index.html' : { -// isBinary: false, -// content : getIframeContent({ title, js, css, html, resources }), -// }, -// 'package.json': { -// content : { -// "description": title, -// "main": "index.html", -// "scripts": { -// "start": "parcel index.html --open", -// "build": "parcel build index.html" -// }, -// "devDependencies": { -// "parcel-bundler": "^2.7" -// } -// }, -// }, -// }, -// }); -// } + + +`, + }, + 'src/index.ts': { + isBinary: false, + content: ` +import './styles.css'; +${packages + .filter(({ imports }) => imports) + .map(({ name, imports }) => `import { ${imports} } from '${name}';`) + .join('\n')} + +${js.replace(/PhotoSphereViewer\./g, '')}`.trim(), + }, + 'src/styles.css': { + isBinary: false, + content: ` +${packages + .filter(({ style }) => style) + .map(({ name }) => `@import '../node_modules/${name}/index.css';`) + .join('\n')} + +${css}`.trim(), + }, + }, + }); +} diff --git a/docs/.vuepress/plugins/gallery/Gallery.vue b/docs/.vuepress/plugins/gallery/Gallery.vue index 48373a344..c528863b1 100644 --- a/docs/.vuepress/plugins/gallery/Gallery.vue +++ b/docs/.vuepress/plugins/gallery/Gallery.vue @@ -1,17 +1,17 @@ diff --git a/docs/.vuepress/plugins/gallery/GalleryItem.vue b/docs/.vuepress/plugins/gallery/GalleryItem.vue index 7302455b2..38f9aa172 100644 --- a/docs/.vuepress/plugins/gallery/GalleryItem.vue +++ b/docs/.vuepress/plugins/gallery/GalleryItem.vue @@ -1,34 +1,34 @@ diff --git a/docs/.vuepress/plugins/gallery/enhanceApp.js b/docs/.vuepress/plugins/gallery/enhanceApp.js index 31064fbe3..a7f1a8410 100644 --- a/docs/.vuepress/plugins/gallery/enhanceApp.js +++ b/docs/.vuepress/plugins/gallery/enhanceApp.js @@ -2,6 +2,6 @@ import Gallery from './Gallery.vue'; import GalleryItem from './GalleryItem.vue'; export default ({ Vue }) => { - Vue.component('Gallery', Gallery); - Vue.component('GalleryItem', GalleryItem); + Vue.component('Gallery', Gallery); + Vue.component('GalleryItem', GalleryItem); }; diff --git a/docs/.vuepress/plugins/gallery/index.js b/docs/.vuepress/plugins/gallery/index.js index 9321847f7..e1a65b37f 100644 --- a/docs/.vuepress/plugins/gallery/index.js +++ b/docs/.vuepress/plugins/gallery/index.js @@ -2,34 +2,32 @@ const container = require('markdown-it-container'); const path = require('path'); module.exports = (options, ctx) => ({ - name : 'gallery', - enhanceAppFiles: path.resolve(__dirname, './enhanceApp.js'), - extendMarkdown : (md) => { - md.use(container, 'gallery', { - render: (tokens, idx) => { - const { nesting } = tokens[idx]; + name: 'gallery', + enhanceAppFiles: path.resolve(__dirname, './enhanceApp.js'), + extendMarkdown: (md) => { + md.use(container, 'gallery', { + render: (tokens, idx) => { + const { nesting } = tokens[idx]; - if (nesting === 1) { - return `\n`; - } - else { - return `\n`; - } - }, - }); + if (nesting === 1) { + return `\n`; + } else { + return `\n`; + } + }, + }); - md.use(container, 'item', { - render: (tokens, idx) => { - const { nesting, info } = tokens[idx]; - const attributes = info.trim().slice('item '.length); + md.use(container, 'item', { + render: (tokens, idx) => { + const { nesting, info } = tokens[idx]; + const attributes = info.trim().slice('item '.length); - if (nesting === 1) { - return `\n`; - } - else { - return `\n`; - } - }, - }); - } + if (nesting === 1) { + return `\n`; + } else { + return `\n`; + } + }, + }); + }, }); diff --git a/docs/.vuepress/plugins/module/index.js b/docs/.vuepress/plugins/module/index.js new file mode 100644 index 000000000..f25264168 --- /dev/null +++ b/docs/.vuepress/plugins/module/index.js @@ -0,0 +1,30 @@ +const container = require('markdown-it-container'); + +module.exports = (options, ctx) => ({ + name: 'module', + extendMarkdown: (md) => { + md.use(container, 'module', { + render: (tokens, idx) => { + const { nesting, info } = tokens[idx]; + const apiLink = info.trim().slice('module '.length); + + if (nesting === 1) { + return `
+${apiLink ? ` + API Documentation + +`: ''}\n`; + } else { + return `
\n`; + } + }, + }); + }, +}); diff --git a/docs/.vuepress/plugins/tabs/index.js b/docs/.vuepress/plugins/tabs/index.js index 2ce4d4fc7..445167c1d 100644 --- a/docs/.vuepress/plugins/tabs/index.js +++ b/docs/.vuepress/plugins/tabs/index.js @@ -1,33 +1,31 @@ const container = require('markdown-it-container'); module.exports = (options, ctx) => ({ - name : 'tabs', - extendMarkdown: (md) => { - md.use(container, 'tabs', { - render: (tokens, idx) => { - const { nesting } = tokens[idx]; + name: 'tabs', + extendMarkdown: (md) => { + md.use(container, 'tabs', { + render: (tokens, idx) => { + const { nesting } = tokens[idx]; - if (nesting === 1) { - return `\n`; - } - else { - return `\n`; - } - }, - }); + if (nesting === 1) { + return `\n`; + } else { + return `\n`; + } + }, + }); - md.use(container, 'tab', { - render: (tokens, idx) => { - const { nesting, info } = tokens[idx]; - const title = info.trim().slice('tab '.length); + md.use(container, 'tab', { + render: (tokens, idx) => { + const { nesting, info } = tokens[idx]; + const title = info.trim().slice('tab '.length); - if (nesting === 1) { - return `\n`; - } - else { - return `\n`; - } - }, - }); - } + if (nesting === 1) { + return `\n`; + } else { + return `\n`; + } + }, + }); + }, }); diff --git a/docs/.vuepress/public/_headers b/docs/.vuepress/public/_headers deleted file mode 100644 index 43ca13680..000000000 --- a/docs/.vuepress/public/_headers +++ /dev/null @@ -1,2 +0,0 @@ -/assets/* - Access-Control-Allow-Origin: * diff --git a/docs/.vuepress/public/_redirects b/docs/.vuepress/public/_redirects index 24a31a601..50f1dbd16 100644 --- a/docs/.vuepress/public/_redirects +++ b/docs/.vuepress/public/_redirects @@ -1,2 +1,14 @@ /guide/adapters/tiles.html /guide/adapters/equirectangular-tiles.html /guide/cropped-panorama.html /guide/adapters/equirectangular.html#cropped-panorama +/plugins/plugin-autorotate-keypoints.html /plugins/autorotate.html +/plugins/plugin-autorotate.html /plugins/autorotate.html +/plugins/plugin-compass.html /plugins/compass.html +/plugins/plugin-gallery.html /plugins/gallery.html +/plugins/plugin-gyroscope.html /plugins/gyroscope.html +/plugins/plugin-markers.html /plugins/markers.html +/plugins/plugin-resolution.html /plugins/resolution.html +/plugins/plugin-settings.html /plugins/settings.html +/plugins/plugin-stereo.html /plugins/stereo.html +/plugins/plugin-video.html /plugins/video.html +/plugins/plugin-virtual-tour.html /plugins/virtual-tour.html +/plugins/plugin-visible-range.html /plugins/visible-range.html diff --git a/docs/.vuepress/theme/components/Home.vue b/docs/.vuepress/theme/components/Home.vue index 0652a2c84..b86bf060a 100644 --- a/docs/.vuepress/theme/components/Home.vue +++ b/docs/.vuepress/theme/components/Home.vue @@ -1,303 +1,285 @@ diff --git a/docs/.vuepress/theme/index.js b/docs/.vuepress/theme/index.js index cc636ec82..a85dc0efa 100644 --- a/docs/.vuepress/theme/index.js +++ b/docs/.vuepress/theme/index.js @@ -1,3 +1,3 @@ module.exports = { - extend: '@vuepress/theme-default' + extend: '@vuepress/theme-default', }; diff --git a/docs/README.md b/docs/README.md index f0b3c628b..41d166183 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,18 +7,18 @@ tagline: A JavaScript library to display Photo Sphere panoramas actionText: Get Started → actionLink: /guide/ features: - - title: Spheres and cubemaps - details: Photo Sphere Viewer can display standard equirectangular panoramas and also cubemaps. - - title: Fully configurable - details: Many options, methods and events allows deep integration in your website/app. - - title: Plugins - details: New plugins add new features without bloating the main library. - - title: Touchscreen, gyroscope and VR - details: Friendly user interactions for all kind of devices. - - title: Markers system - details: Display texts, images and even dynamic areas directly on your photos. - - title: Videos - details: Photo Sphere Viewer also supports video, both equirectangular and cubemaps. + - title: Spheres and cubemaps + details: Photo Sphere Viewer can display standard equirectangular panoramas and also cubemaps. + - title: Fully configurable + details: Many options, methods and events allows deep integration in your website/app. + - title: Plugins + details: New plugins add new features without bloating the main library. + - title: Touchscreen, gyroscope and VR + details: Friendly user interactions for all kind of devices. + - title: Markers system + details: Display texts, images and even dynamic areas directly on your photos. + - title: Videos + details: Photo Sphere Viewer also supports video, both equirectangular and cubemaps. --- @@ -27,7 +27,6 @@ features: I forked the original Photo Sphere Viewer [by Jérémy Heleine](http://jeremyheleine.me) to provide a better code architecture and a bunch of new features. ::: - ::: slot footer Licensed under MIT License, documentation under CC BY 3.0 diff --git a/docs/demos/README.md b/docs/demos/README.md index ec4fe0592..49bb1f7a0 100644 --- a/docs/demos/README.md +++ b/docs/demos/README.md @@ -10,56 +10,63 @@ Visit each [plugin page](../plugins/) to see a dedicated example. ::: item [![](../images/demos/default.jpg)](./basic/1-zero-config.md) + ### Zero config + Simple panorama with default options. ::: ::: item [![](../images/demos/description.jpg)](./basic/2-description.md) + ### Description + « i » button will display the `description` in the side panel. ::: ::: item [![](../images/demos/navbar.jpg)](./basic/3-custom-navbar.md) + ### Custom navbar + Customize the navbar with default and custom buttons. ::: ::: item [![](../images/demos/fisheye.jpg)](./basic/4-fisheye.md) + ### Fisheye + Display the panorama with a fisheye effect. ::: ::: item -[![](../images/demos/autorotate.gif)](./basic/5-autorotate.md) -### Autorotate -Automatically performs a rotation if the user is inactive. -::: +[![](../images/demos/animation.gif)](./basic/5-animation.md) -::: item -[![](../images/demos/animation.gif)](./basic/6-animation.md) ### Intro animation + Use the `Animation` helper to create a cool intro. ::: ::: item -[![](../images/demos/overlay.jpg)](./basic/7-overlay.md) +[![](../images/demos/overlay.jpg)](./basic/6-overlay.md) + ### Overlay + Display a secondary image above the panorama. ::: :::: - ## Markers :::: gallery ::: item [![](../images/demos/custom-marker.jpg)](./markers/custom-tooltip.md) + ### Custom tooltip + Advanced styling of a marker's tooltip. ::: diff --git a/docs/demos/basic/1-zero-config.md b/docs/demos/basic/1-zero-config.md index 0b4f999aa..976b85e7c 100644 --- a/docs/demos/basic/1-zero-config.md +++ b/docs/demos/basic/1-zero-config.md @@ -1,6 +1,6 @@ # Zero config -Simple panorama with default options. +Simple panorama with minimal options. ::: code-demo @@ -12,9 +12,8 @@ title: PSV Basic Demo const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', }); ``` diff --git a/docs/demos/basic/2-description.md b/docs/demos/basic/2-description.md index ac2eba674..c46232b77 100644 --- a/docs/demos/basic/2-description.md +++ b/docs/demos/basic/2-description.md @@ -12,27 +12,27 @@ title: PSV Description Demo const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - description: document.querySelector('#description').innerHTML, - navbar: 'caption description', + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + description: document.querySelector('#description').innerHTML, + navbar: 'caption description', }); ``` ```html ``` diff --git a/docs/demos/basic/3-custom-navbar.md b/docs/demos/basic/3-custom-navbar.md index b4070b7fe..a2e46ce94 100644 --- a/docs/demos/basic/3-custom-navbar.md +++ b/docs/demos/basic/3-custom-navbar.md @@ -12,31 +12,31 @@ title: PSV Navbar Demo const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - navbar: [ - 'zoom', - 'move', - { - title : 'Change image', - content : document.querySelector('#icon').innerText, - onClick : function () { - this.setPanorama(baseUrl + 'sphere-test.jpg', { - caption : '', - description: null, - }); - }, - }, - 'caption', - 'fullscreen', - ], + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + navbar: [ + 'zoom', + 'move', + { + title: 'Change image', + content: document.querySelector('#icon').innerText, + onClick (viewer) { + viewer.setPanorama(baseUrl + 'sphere-test.jpg', { + caption: '', + description: null, + }); + }, + }, + 'caption', + 'fullscreen', + ], }); ``` ```html ``` diff --git a/docs/demos/basic/4-fisheye.md b/docs/demos/basic/4-fisheye.md index 3aa72bc11..a0cec1d83 100644 --- a/docs/demos/basic/4-fisheye.md +++ b/docs/demos/basic/4-fisheye.md @@ -12,12 +12,12 @@ title: PSV Fisheye Demo const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - defaultLat: 0.6, - defaultZoomLvl: 20, - fisheye: true, + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + defaultPitch: 0.6, + defaultZoomLvl: 20, + fisheye: true, }); ``` diff --git a/docs/demos/basic/5-animation.md b/docs/demos/basic/5-animation.md new file mode 100644 index 000000000..7c1849602 --- /dev/null +++ b/docs/demos/basic/5-animation.md @@ -0,0 +1,74 @@ +# Intro animation + +Use the `Animation` helper to create a cool intro. + +::: code-demo + +```yaml +title: PSV Intro Animation Demo +packages: + - name: autorotate-plugin + imports: AutorotatePlugin +``` + +```js +const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; + +const animatedValues = { + pitch: { start: -Math.PI / 2, end: 0.2 }, + yaw: { start: Math.PI, end: 0 }, + zoom: { start: 0, end: 50 }, + fisheye: { start: 2, end: 0 }, +}; + +const viewer = new PhotoSphereViewer.Viewer({ + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + defaultPitch: animatedValues.pitch.start, + defaultYaw: animatedValues.yaw.start, + defaultZoomLvl: animatedValues.zoom.start, + fisheye: animatedValues.fisheye.start, + navbar: [ + 'autorotate', + 'zoom', + { + title: 'Rerun animation', + content: '🔄', + onClick: intro, + }, + 'caption', + 'fullscreen', + ], + plugins: [ + [PhotoSphereViewer.AutorotatePlugin, { + autostartDelay: null, + autostartOnIdle: false, + autorotatePitch: animatedValues.pitch.end, + }], + ], +}); + +const autorotate = viewer.getPlugin(PhotoSphereViewer.AutorotatePlugin); + +viewer.addEventListener('ready', intro, { once: true }); + +function intro() { + autorotate.stop(); + + new PhotoSphereViewer.utils.Animation({ + properties: animatedValues, + duration: 2500, + easing: 'inOutQuad', + onTick: (properties) => { + viewer.setOption('fisheye', properties.fisheye); + viewer.rotate({ yaw: properties.yaw, pitch: properties.pitch }); + viewer.zoom(properties.zoom); + }, + }).then(() => { + autorotate.start(); + }); +} +``` + +::: diff --git a/docs/demos/basic/5-autorotate.md b/docs/demos/basic/5-autorotate.md deleted file mode 100644 index ed6d8b541..000000000 --- a/docs/demos/basic/5-autorotate.md +++ /dev/null @@ -1,24 +0,0 @@ -# Autorotate - -Automatically performs a rotation if the user is inactive. - -::: code-demo - -```yaml -title: PSV Autorotate Demo -``` - -```js -const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; - -new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - autorotateDelay: 1000, - autorotateIdle: true, -}); -``` - -::: - diff --git a/docs/demos/basic/6-animation.md b/docs/demos/basic/6-animation.md deleted file mode 100644 index fc835952f..000000000 --- a/docs/demos/basic/6-animation.md +++ /dev/null @@ -1,67 +0,0 @@ -# Intro animation - -Use the `Animation` helper to create a cool intro. - -::: code-demo - -```yaml -title: PSV Intro Animation Demo -``` - -```js -const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; - -const animatedValues = { - latitude : { start: -Math.PI / 2, end: 0.2 }, - longitude: { start: Math.PI, end: 0 }, - zoom : { start: 0, end: 50 }, - fisheye : { start: 2, end: 0 }, -}; - -const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - defaultLat: animatedValues.latitude.start, - defaultLong: animatedValues.longitude.start, - defaultZoomLvl: animatedValues.zoom.start, - fisheye: animatedValues.fisheye.start, - navbar: [ - 'zoom', - { - title: 'Rerun animation', - content: '🔄', - onClick: intro, - }, - 'caption', - 'fullscreen' - ], -}); - -viewer.on('ready', intro); - -function intro() { - viewer.stopAutorotate(); - - new PhotoSphereViewer.utils.Animation({ - properties: animatedValues, - duration: 2500, - easing: 'inOutQuad', - onTick: (properties) => { - viewer.setOption('fisheye', properties.fisheye); - viewer.rotate({ longitude: properties.longitude, latitude: properties.latitude }); - viewer.zoom(properties.zoom); - } - }) - .then(() => { - viewer.setOptions({ - autorotateLat: animatedValues.latitude.end, - autorotateDelay: 1000, - autorotateIdle: true, - }); - viewer.startAutorotate(); - }); -} -``` - -::: diff --git a/docs/demos/basic/7-overlay.md b/docs/demos/basic/6-overlay.md similarity index 53% rename from docs/demos/basic/7-overlay.md rename to docs/demos/basic/6-overlay.md index 980a0deae..32bd88cc3 100644 --- a/docs/demos/basic/7-overlay.md +++ b/docs/demos/basic/6-overlay.md @@ -12,11 +12,11 @@ title: PSV Overlay Demo const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - overlay: baseUrl + 'sphere-overlay.png', - overlayOpacity: 0.8, + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + overlay: baseUrl + 'sphere-overlay.png', + overlayOpacity: 0.8, }); ``` diff --git a/docs/demos/markers/custom-tooltip.md b/docs/demos/markers/custom-tooltip.md index f0c1f026d..5fb84ed48 100644 --- a/docs/demos/markers/custom-tooltip.md +++ b/docs/demos/markers/custom-tooltip.md @@ -6,86 +6,85 @@ Advanced styling of a marker's tooltip. ```yaml title: PSV Marker custom tooltip Demo -resources: - - path: plugins/markers.js - imports: MarkersPlugin - - path: plugins/markers.css +packages: + - name: markers-plugin + imports: MarkersPlugin + style: true ``` ```js const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - - plugins: [ - [PhotoSphereViewer.MarkersPlugin, { - // list of markers - markers: [{ - id : 'custom-tooltip', - tooltip : { - content : document.querySelector('#tooltip-content').innerText, - className: 'custom-tooltip', - position : 'top', - trigger : 'click', - }, - latitude : 0.11, - longitude: -0.35, - image : baseUrl + 'pictos/pin-blue.png', - width : 32, - height : 32, - anchor : 'bottom center', - }], - }], - ], + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + + plugins: [ + [PhotoSphereViewer.MarkersPlugin, { + markers: [{ + id: 'custom-tooltip', + tooltip: { + content: document.querySelector('#tooltip-content').innerText, + className: 'custom-tooltip', + position: 'top', + trigger: 'click', + }, + position: { pitch: 0.11, yaw: -0.35 }, + image: baseUrl + 'pictos/pin-blue.png', + size: { width: 32, height: 32 }, + anchor: 'bottom center', + }], + }], + ], }); const markersPlugin = viewer.getPlugin(PhotoSphereViewer.MarkersPlugin); -viewer.once('ready', () => { - viewer.animate({ - longitude: 0, - latitude: 0.5, - speed: 1000, - }) - .then(() => { - markersPlugin.showMarkerTooltip('custom-tooltip'); - }); -}); +viewer.addEventListener('ready', () => { + viewer + .animate({ + longitude: 0, + latitude: 0.5, + speed: 1000, + }) + .then(() => { + markersPlugin.showMarkerTooltip('custom-tooltip'); + }); +}, { once: true }); ``` ```css .custom-tooltip { - max-width: none; - width: 300px; - padding: 0; - box-shadow: 0 0 0 3px white; + max-width: none; + width: 300px; + padding: 0; + box-shadow: 0 0 0 3px white; } .custom-tooltip img { - width: 100%; - border-radius: 4px 4px 0 0; + width: 100%; + border-radius: 4px 4px 0 0; } -.custom-tooltip h2, .custom-tooltip p { - margin: 1rem; - text-align: justify; +.custom-tooltip h2, +.custom-tooltip p { + margin: 1rem; + text-align: justify; } ``` ```html ``` diff --git a/docs/guide/README.md b/docs/guide/README.md index 289d99e04..18e953052 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -3,7 +3,7 @@ [[toc]] ::: danger New version -Photo Sphere Viewer 4 is not compatible with previous versions. If you are using version 3 , please follow the [migration guide](./migration-v3.md). You can also read the [version 3 documentation](https://photo-sphere-viewer-3.netlify.com). +From version 5, Photo Sphere Viewer is now available under the name `@photo-sphere-viewer/core` an other sub-packages. If you are using the previous `photo-sphere-viewer` package, please follow the [migration guide](./migration.md). You can also read the [version 4 documentation](https://photo-sphere-viewer-4.netlify.app/guide/). ::: ::: tip Playground @@ -15,14 +15,14 @@ Test Photo Sphere Viewer with you own panorama in the [Playground](../playground #### With npm or yarn ```bash -npm install photo-sphere-viewer +npm install @photo-sphere-viewer/core -yarn add photo-sphere-viewer +yarn add @photo-sphere-viewer/core ``` #### Via CDN -Photo Sphere Viewer is available on [jsDelivr](https://www.jsdelivr.com/package/npm/photo-sphere-viewer) +Photo Sphere Viewer is available on [jsDelivr](https://www.jsdelivr.com/package/npm/@photo-sphere-viewer/core) #### Manually @@ -30,9 +30,7 @@ You can also [download the latest release](https://github.com/mistic100/Photo-Sp ## Dependencies - * [Three.js](https://threejs.org) (use `build/three.min.js` file) - * [uEvent 2](https://github.com/mistic100/uEvent) (use `browser.js` file) - +- [Three.js](https://threejs.org) (use `build/three.min.js` file) ## Your first viewer @@ -41,37 +39,38 @@ Include all JS & CSS files in your page manually or with your favorite bundler a :::: tabs ::: tab Direct import + ```html - - + + - + - - +
``` + ::: ::: tab ES import -Import `photo-sphere-viewer/dist/photo-sphere-viewer.css` with the prefered way depending on your tooling. +Import `@photo-sphere-viewer/core/index.css` with the prefered way depending on your tooling. ```html - - + + @@ -79,13 +78,14 @@ Import `photo-sphere-viewer/dist/photo-sphere-viewer.css` with the prefered way ``` ```js -import { Viewer } from 'photo-sphere-viewer'; +import { Viewer } from '@photo-sphere-viewer/core'; const viewer = new Viewer({ - container: document.querySelector('#viewer'), - panorama: 'path/to/panorama.jpg', + container: document.querySelector('#viewer'), + panorama: 'path/to/panorama.jpg', }); ``` + ::: :::: @@ -102,18 +102,17 @@ title: PSV Basic Demo const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, }); ``` ::: - The `panorama` must be an [equirectangular projection](https://en.wikipedia.org/wiki/Equirectangular_projection) of your photo. Other modes are supported through [adapters](./adapters/). ::: tip Cropped panoramas diff --git a/docs/guide/adapters/README.md b/docs/guide/adapters/README.md index 3ddbeb9d8..395c60966 100644 --- a/docs/guide/adapters/README.md +++ b/docs/guide/adapters/README.md @@ -3,49 +3,55 @@ Adapters are small pieces of code responsible to load the panorama texture(s) in the Three.js scene. The supported adapters are: -- [equirectangular](equirectangular.md): the default adapter, used to load full or partial equirectangular panoramas -- [equirectangular tiles](equirectangular-tiles.md): used to load tiled equirectangular panoramas -- [equirectangular video](equirectangular-video.md): used to load equirectangular videos -- [cubemap](cubemap.md): used to load cubemaps projections (six textures) -- [cubemap tiles](cubemap-tiles.md): used to load tiled cubemap panoramas -- [cubemap video](cubemap-video.md): used to load cubemap video + +- [equirectangular](equirectangular.md): the default adapter, used to load full or partial equirectangular panoramas +- [equirectangular tiles](equirectangular-tiles.md): used to load tiled equirectangular panoramas +- [equirectangular video](equirectangular-video.md): used to load equirectangular videos +- [cubemap](cubemap.md): used to load cubemaps projections (six textures) +- [cubemap tiles](cubemap-tiles.md): used to load tiled cubemap panoramas +- [cubemap video](cubemap-video.md): used to load cubemap video +- [little planet](little-planet.md): used to display equirectangular panoramas with a little planet effect ## Import an adapter -Official adapters are available in the the main `photo-sphere-viewer` package inside the `dist/adapters` directory. +Official adapters are available in various `@photo-sphere-viewer/***-adapter` packages. **Example for the Cubemap adapter:** :::: tabs ::: tab Direct import + ```html - + ``` ```js new PhotoSphereViewer.Viewer({ - adapter: [PhotoSphereViewer.CubemapAdapter, { - // optional adapter config - }], - panorama: // specific to the adapter, + adapter: [PhotoSphereViewer.CubemapAdapter, { + // optional adapter config + }], + panorama: // specific to the adapter, }); ``` + ::: ::: tab ES import + ```js -import { CubemapAdapter } from 'photo-sphere-viewer/dist/adapters/cubemap'; +import { CubemapAdapter } from '@photo-sphere-viewer/cubemap-adapter'; new Viewer({ - adapter: [CubemapAdapter, { - // optional adapter config - }], - panorama: // specific to the adapter, + adapter: [CubemapAdapter, { + // optional adapter config + }], + panorama: // specific to the adapter, }); ``` + ::: :::: diff --git a/docs/guide/adapters/cubemap-tiles.md b/docs/guide/adapters/cubemap-tiles.md index fe477674a..4211d1ee4 100644 --- a/docs/guide/adapters/cubemap-tiles.md +++ b/docs/guide/adapters/cubemap-tiles.md @@ -1,120 +1,125 @@ # Cubemap tiles -> Reduce the initial loading time and used bandwidth by slicing big cubemap panoramas into many small tiles. +::: module +Reduce the initial loading time and used bandwidth by slicing big cubemap panoramas into many small tiles. -This adapter is available in the core `photo-sphere-viewer` package in `dist/adapters/cubemap-tiles.js`. It requires `dist/adapters/cubemap.js` to be loaded too. +This adapter is available in the `@photo-sphere-viewer/cubemap-tiles-adapter` package. It requires `@photo-sphere-viewer/cubemap-adapter` to be loaded too. +::: ```js const viewer = new PhotoSphereViewer.Viewer({ - adapter: PhotoSphereViewer.CubemapTilesAdapter, - panorama: { - faceSize: 6000, - nbTiles: 8, - baseUrl: { - left : 'left_low.jpg', - front : 'front_low.jpg', - right : 'right_low.jpg', - back : 'back_low.jpg', - top : 'top_low.jpg', - bottom: 'bottom_low.jpg', - }, - tileUrl: (face, col, row) => { - return `${face}_${col}_${row}.jpg`; + adapter: PhotoSphereViewer.CubemapTilesAdapter, + panorama: { + faceSize: 6000, + nbTiles: 8, + baseUrl: { + left: 'left_low.jpg', + front: 'front_low.jpg', + right: 'right_low.jpg', + back: 'back_low.jpg', + top: 'top_low.jpg', + bottom: 'bottom_low.jpg', + }, + tileUrl: (face, col, row) => { + return `${face}_${col}_${row}.jpg`; + }, }, - }, }); ``` - ## Example ::: code-demo ```yaml title: PSV Cubemap Tiles Demo -resources: - - path: adapters/cubemap.js - - path: adapters/cubemap-tiles.js - imports: CubemapTilesAdapter +packages: + - name: cubemap-adapter + - name: cubemap-tiles-adapter + imports: CubemapTilesAdapter ``` ```js const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - adapter: PhotoSphereViewer.CubemapTilesAdapter, - panorama: { - faceSize: 1500, - nbTiles : 4, - baseUrl : { - left : baseUrl + 'cubemap/px.jpg', - front : baseUrl + 'cubemap/nz.jpg', - right : baseUrl + 'cubemap/nx.jpg', - back : baseUrl + 'cubemap/pz.jpg', - top : baseUrl + 'cubemap/py.jpg', - bottom: baseUrl + 'cubemap/ny.jpg' - }, - tileUrl : (face, col, row) => { - const num = row * 4 + col; - return `${baseUrl}cubemap-tiles/${face}_${('00' + num).slice(-2)}.jpg`; + container: 'viewer', + adapter: PhotoSphereViewer.CubemapTilesAdapter, + panorama: { + faceSize: 1500, + nbTiles: 4, + baseUrl: { + left: baseUrl + 'cubemap/px.jpg', + front: baseUrl + 'cubemap/nz.jpg', + right: baseUrl + 'cubemap/nx.jpg', + back: baseUrl + 'cubemap/pz.jpg', + top: baseUrl + 'cubemap/py.jpg', + bottom: baseUrl + 'cubemap/ny.jpg', + }, + tileUrl: (face, col, row) => { + const num = row * 4 + col; + return `${baseUrl}cubemap-tiles/${face}_${('00' + num).slice(-2)}.jpg`; + }, }, - }, - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, }); ``` ::: - ## Configuration #### `flipTopBottom` -- type: `boolean` -- default: `false` + +- type: `boolean` +- default: `false` Set to `true` if the top and bottom faces are not correctly oriented. #### `baseBlur` -- type: `boolean` -- default: `true` + +- type: `boolean` +- default: `true` Applies a 1px blur to the base image (option `baseUrl`). #### `showErrorTile` -- type: `boolean` -- default: `true` -Shows a warning sign on tiles that cannot be loaded. +- type: `boolean` +- default: `true` +Shows a warning sign on tiles that cannot be loaded. ## Panorama options -When using this adapter the `panorama` option and the `setPanorama()` method accept an object to configure the tiles. +When using this adapter, the `panorama` option and the `setPanorama()` method accept an object to configure the tiles. #### `faceSize` (required) -- type: `number` + +- type: `number` Size in pixel of a face of the cube. #### `nbTiles` (required) -- type: `number` + +- type: `number` Number of columns and rows on a face. Each tile must be square. Must be power of two (2, 4, 8, 16) and the maximum value is 16. #### `tileUrl` (required) -- type: `function: (face, col, row) => string` + +- type: `function: (face, col, row) => string` Function used to build the URL of a tile. `face` will be one of `'left'|'front'|'right'|'back'|'top'|'bottom'`. #### `baseUrl` (recommended) -- type: `string[] | Record` -URL of a low resolution complete panorama image to display while the tiles are loading. It accepts the same format as the standard [cubemap adapter](./cubemap.md#panorama-options). +- type: `string[] | Record` +URL of a low resolution complete panorama image to display while the tiles are loading. It accepts the same format as the standard [cubemap adapter](./cubemap.md#panorama-options). ## Preparing the panorama @@ -132,7 +137,6 @@ magick.exe front.jpg \ You can also use this [online tool](https://pinetools.com/split-image). - ::: tip Performances It is recommanded to not exceed tiles with a size of 1024x1024 pixels, thus limiting the maximum panorama size to 16.384x16.384 pixels by face (1.6 Gigapixels in total). ::: diff --git a/docs/guide/adapters/cubemap-video.md b/docs/guide/adapters/cubemap-video.md index 43b2c0c91..f7eca18a5 100644 --- a/docs/guide/adapters/cubemap-video.md +++ b/docs/guide/adapters/cubemap-video.md @@ -1,115 +1,117 @@ # Cubemap video +::: module +This adapter is available in the `@photo-sphere-viewer/cubemap-video-adapter` package. +::: + ```js const viewer = new PhotoSphereViewer.Viewer({ - adapter: PhotoSphereViewer.CubemapVideoAdapter, - panorama: { - source: 'path/video.mp4', // also supports webm - }, - plugins: [ - PhotoSphereViewer.VideoPlugin, - ], + adapter: PhotoSphereViewer.CubemapVideoAdapter, + panorama: { + source: 'path/video.mp4', // also supports webm + }, + plugins: [PhotoSphereViewer.VideoPlugin], }); ``` ::: warning -This adapter requires to use the [VideoPlugin](../../plugins/plugin-video.md). +This adapter requires to use the [VideoPlugin](../../plugins/video.md). ::: - ## Example ::: code-demo ```yaml title: PSV Cubemap Video Demo -resources: - - path: adapters/cubemap-video.js - imports: CubemapVideoAdapter - - path: plugins/video.js - imports: VideoPlugin - - path: plugins/video.css - - path: plugins/settings.js - imports: SettingsPlugin - - path: plugins/settings.css - - path: plugins/resolution.js - imports: ResolutionPlugin +packages: + - name: cubemap-video-adapter + imports: CubemapVideoAdapter + - name: video-plugin + imports: VideoPlugin + style: true + - name: settings-plugin + imports: SettingsPlugin + style: true + - name: resolution-plugin + imports: ResolutionPlugin ``` ```js const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - adapter: [PhotoSphereViewer.CubemapVideoAdapter, { - muted: true, - }], - caption: 'Dreams of Dalí © The Dalí Museum', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - navbar: 'video autorotate caption settings fullscreen', - - plugins: [ - PhotoSphereViewer.VideoPlugin, - PhotoSphereViewer.SettingsPlugin, - [PhotoSphereViewer.ResolutionPlugin, { - defaultResolution: 'HD', - resolutions: [ - { - id : 'UHD', - label : 'Ultra high', - panorama: { source: baseUrl + 'cubemap-video/DreamOfDali_UHD.webm' }, - }, - { - id : 'FHD', - label : 'High', - panorama: { source: baseUrl + 'cubemap-video/DreamOfDali_FHD.webm' }, - }, - { - id : 'HD', - label : 'Standard', - panorama: { source: baseUrl + 'cubemap-video/DreamOfDali_HD.webm' }, - }, - ] - }] - ], + container: 'viewer', + adapter: [PhotoSphereViewer.CubemapVideoAdapter, { + muted: true, + }], + caption: 'Dreams of Dalí © The Dalí Museum', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + navbar: 'video caption settings fullscreen', + + plugins: [ + PhotoSphereViewer.VideoPlugin, + PhotoSphereViewer.SettingsPlugin, + [PhotoSphereViewer.ResolutionPlugin, { + defaultResolution: 'HD', + resolutions: [ + { + id: 'UHD', + label: 'Ultra high', + panorama: { source: baseUrl + 'cubemap-video/DreamOfDali_UHD.webm' }, + }, + { + id: 'FHD', + label: 'High', + panorama: { source: baseUrl + 'cubemap-video/DreamOfDali_FHD.webm' }, + }, + { + id: 'HD', + label: 'Standard', + panorama: { source: baseUrl + 'cubemap-video/DreamOfDali_HD.webm' }, + }, + ], + }], + ], }); ``` ::: - ## Configuration #### `autoplay` -- type: `boolean` -- default: `false` + +- type: `boolean` +- default: `false` Automatically starts the video on load. #### `muted` -- type: `boolean` -- default: `false` (`true` if `autoplay=true`) + +- type: `boolean` +- default: `false` Mute the video by default. #### `equiangular` -- type: `boolean` -- default: `true` -Set to `true` when using an equiangular cubemap (EAC), which is the format used by Youtube. Set to `false` when using a standard cubemap. +- type: `boolean` +- default: `true` +Set to `true` when using an equiangular cubemap (EAC), which is the format used by Youtube. Set to `false` when using a standard cubemap. ## Panorama options -When using this adapter the `panorama` option and the `setPanorama()` method accept an object to configure the video. +When using this adapter, the `panorama` option and the `setPanorama()` method accept an object to configure the video. #### `source` (required) -- type: `string` -Path of the video file. The video must not be larger than 4096 pixels or it won't be displayed on handled devices. +- type: `string` +Path of the video file. The video must not be larger than 4096 pixels or it won't be displayed on handled devices. ### Video format diff --git a/docs/guide/adapters/cubemap.md b/docs/guide/adapters/cubemap.md index 792ea08e0..c0fb70443 100644 --- a/docs/guide/adapters/cubemap.md +++ b/docs/guide/adapters/cubemap.md @@ -1,74 +1,71 @@ # Cubemap -> [Cube mapping](https://en.wikipedia.org/wiki/Cube_mapping) is a kind of projection where the environment is mapped to the six faces of a cube around the viewer. +::: module +[Cube mapping](https://en.wikipedia.org/wiki/Cube_mapping) is a kind of projection where the environment is mapped to the six faces of a cube around the viewer. -This adapter is available in the core `photo-sphere-viewer` package in `dist/adapters/cubemap.js`. - -Photo Sphere Viewer supports cubemaps as six distinct image files. The files can be provided as an object or an array. +This adapter is available in the `@photo-sphere-viewer/cubemap-adapter` package. +::: ```js const viewer = new PhotoSphereViewer.Viewer({ - adapter: PhotoSphereViewer.CubemapAdapter, - panorama: { - left: 'path/to/left.jpg', - front: 'path/to/front.jpg', - right: 'path/to/right.jpg', - back: 'path/to/back.jpg', - top: 'path/to/top.jpg', - bottom: 'path/to/bottom.jpg', - }, + adapter: PhotoSphereViewer.CubemapAdapter, + panorama: { + left: 'path/to/left.jpg', + front: 'path/to/front.jpg', + right: 'path/to/right.jpg', + back: 'path/to/back.jpg', + top: 'path/to/top.jpg', + bottom: 'path/to/bottom.jpg', + }, }); ``` - ## Example ::: code-demo ```yaml title: PSV Cubemap Demo -resources: - - path: adapters/cubemap.js - imports: CubemapAdapter +packages: + - name: cubemap-adapter + imports: CubemapAdapter ``` ```js const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - adapter: PhotoSphereViewer.CubemapAdapter, - panorama: { - left : baseUrl + 'cubemap/px.jpg', - front : baseUrl + 'cubemap/nz.jpg', - right : baseUrl + 'cubemap/nx.jpg', - back : baseUrl + 'cubemap/pz.jpg', - top : baseUrl + 'cubemap/py.jpg', - bottom: baseUrl + 'cubemap/ny.jpg' - }, - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - defaultLat: 0.2, - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, + container: 'viewer', + adapter: PhotoSphereViewer.CubemapAdapter, + panorama: { + left: baseUrl + 'cubemap/px.jpg', + front: baseUrl + 'cubemap/nz.jpg', + right: baseUrl + 'cubemap/nx.jpg', + back: baseUrl + 'cubemap/pz.jpg', + top: baseUrl + 'cubemap/py.jpg', + bottom: baseUrl + 'cubemap/ny.jpg', + }, + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, }); ``` ::: - ## Configuration #### `flipTopBottom` -- type: `boolean` -- default: `false` -Set to `true` if the top and bottom faces are not correctly oriented. +- type: `boolean` +- default: `false` +Set to `true` if the top and bottom faces are not correctly oriented. ## Panorama options -When using this adapter the `panorama` option and the `setPanorama()` method accept an array or an object of six URLs. +When using this adapter, the `panorama` option and the `setPanorama()` method accept an array or an object of six URLs. ```js // Cubemap as array (order is important) : diff --git a/docs/guide/adapters/equirectangular-tiles.md b/docs/guide/adapters/equirectangular-tiles.md index a7145d770..8b9467dc8 100644 --- a/docs/guide/adapters/equirectangular-tiles.md +++ b/docs/guide/adapters/equirectangular-tiles.md @@ -1,119 +1,126 @@ # Equirectangular tiles -> Reduce the initial loading time and used bandwidth by slicing big equirectangular panoramas into many small tiles. +::: module +Reduce the initial loading time and used bandwidth by slicing big equirectangular panoramas into many small tiles. -This adapter is available in the core `photo-sphere-viewer` package in `dist/adapters/equirectangular-tiles.js`. +This adapter is available in the `@photo-sphere-viewer/equirectangular-tiles-adapter` package. +::: ```js const viewer = new PhotoSphereViewer.Viewer({ - adapter: PhotoSphereViewer.EquirectangularTilesAdapter, - panorama: { - width: 12000, - cols: 16, - rows: 8, - baseUrl: 'panorama_low.jpg', - tileUrl: (col, row) => { - return `panorama_${col}_${row}.jpg`; + adapter: PhotoSphereViewer.EquirectangularTilesAdapter, + panorama: { + width: 12000, + cols: 16, + rows: 8, + baseUrl: 'panorama_low.jpg', + tileUrl: (col, row) => { + return `panorama_${col}_${row}.jpg`; + }, }, - }, }); ``` - ## Example ::: code-demo ```yaml title: PSV Equirectangular Tiles Demo -resources: - - path: adapters/equirectangular-tiles.js - imports: EquirectangularTilesAdapter +packages: + - name: equirectangular-tiles-adapter + imports: EquirectangularTilesAdapter ``` ```js const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - adapter: PhotoSphereViewer.EquirectangularTilesAdapter, - panorama: { - width : 6656, - cols : 16, - rows : 8, - baseUrl: `${baseUrl}sphere-small.jpg`, - tileUrl: (col, row) => { - const num = row * 16 + col + 1; - return `${baseUrl}sphere-tiles/image_part_${('000' + num).slice(-3)}.jpg`; + container: 'viewer', + adapter: PhotoSphereViewer.EquirectangularTilesAdapter, + panorama: { + width: 6656, + cols: 16, + rows: 8, + baseUrl: `${baseUrl}sphere-small.jpg`, + tileUrl: (col, row) => { + const num = row * 16 + col + 1; + return `${baseUrl}sphere-tiles/image_part_${('000' + num).slice(-3)}.jpg`; + }, }, - }, - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, }); ``` ::: - ## Configuration #### `baseBlur` -- type: `boolean` -- default: `true` + +- type: `boolean` +- default: `true` Applies a 1px blur to the base image (option `baseUrl`). #### `showErrorTile` -- type: `boolean` -- default: `true` + +- type: `boolean` +- default: `true` Shows a warning sign on tiles that cannot be loaded. #### `resolution` -- type: `number` -- default: `64` + +- type: `number` +- default: `64` The number of faces of the sphere geometry used to display the panorama, higher values can reduce deformations on straight lines at the cost of performances. _Note: the actual number of faces is `resolution² / 2`._ - ## Panorama options -When using this adapter the `panorama` option and the `setPanorama()` method accept an object to configure the tiles. +When using this adapter, the `panorama` option and the `setPanorama()` method accept an object to configure the tiles. #### `width` (required) -- type: `number` + +- type: `number` Total width of the panorama, the height is always width / 2. #### `cols` (required) -- type: `number` + +- type: `number` Number of columns, must be power of two (4, 8, 16, 32, 64) and the maximum value is 64. #### `rows` (required) -- type: `number` + +- type: `number` Number of rows, must be power of two (2, 4, 8, 16, 32) and the maximum value is 32. #### `tileUrl` (required) -- type: `function: (col, row) => string` + +- type: `function: (col, row) => string` Function used to build the URL of a tile. #### `baseUrl` (recommended) -- type: `string` + +- type: `string` URL of a low resolution complete panorama image to display while the tiles are loading. #### `basePanoData` -- type: `object | function` -Panorama configuration associated to low resolution first image, following the same format as [`panoData` configuration object](../config.md#panodata) +- type: `object | function` +Panorama configuration associated to low resolution first image, following the same format as [`panoData` configuration object](../config.md#panodata) ## Preparing the panorama @@ -131,7 +138,6 @@ magick.exe panorama.jpg \ You can also use this [online tool](https://pinetools.com/split-image). - ::: tip Performances It is recommanded to not exceed tiles with a size of 1024x1024 pixels, thus limiting the maximum panorama size to 65.536x32.768 pixels (2 Gigapixels). ::: diff --git a/docs/guide/adapters/equirectangular-video.md b/docs/guide/adapters/equirectangular-video.md index 42510f6e5..1e6955dc8 100644 --- a/docs/guide/adapters/equirectangular-video.md +++ b/docs/guide/adapters/equirectangular-video.md @@ -1,118 +1,121 @@ # Equirectangular video +::: module +This adapter is available in the `@photo-sphere-viewer/equirectangular-video-adapter` package. +::: + ```js const viewer = new PhotoSphereViewer.Viewer({ - adapter: PhotoSphereViewer.EquirectangularVideoAdapter, - panorama: { - source: 'path/video.mp4', // also supports webm - }, - plugins: [ - PhotoSphereViewer.VideoPlugin, - ], + adapter: PhotoSphereViewer.EquirectangularVideoAdapter, + panorama: { + source: 'path/video.mp4', // also supports webm + }, + plugins: [PhotoSphereViewer.VideoPlugin], }); ``` ::: warning -This adapter requires to use the [VideoPlugin](../../plugins/plugin-video.md). +This adapter requires to use the [VideoPlugin](../../plugins/video.md). ::: - ## Example ::: code-demo ```yaml title: PSV Equirectangular Video Demo -resources: - - path: adapters/equirectangular-video.js - imports: EquirectangularVideoAdapter - - path: plugins/video.js - imports: VideoPlugin - - path: plugins/video.css - - path: plugins/settings.js - imports: SettingsPlugin - - path: plugins/settings.css - - path: plugins/resolution.js - imports: ResolutionPlugin +packages: + - name: equirectangular-video-adapter + imports: EquirectangularVideoAdapter + - name: video-plugin + imports: VideoPlugin + style: true + - name: settings-plugin + imports: SettingsPlugin + style: true + - name: resolution-plugin + imports: ResolutionPlugin ``` ```js const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - adapter: [PhotoSphereViewer.EquirectangularVideoAdapter, { - muted: true, - }], - caption: 'Ayutthaya © meetle', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - navbar: 'video autorotate caption settings fullscreen', - - plugins: [ - PhotoSphereViewer.VideoPlugin, - PhotoSphereViewer.SettingsPlugin, - [PhotoSphereViewer.ResolutionPlugin, { - defaultResolution: 'HD', - resolutions: [ - { - id : 'UHD', - label : 'Ultra high', - panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_UHD.mp4' }, - }, - { - id : 'FHD', - label : 'High', - panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_FHD.mp4' }, - }, - { - id : 'HD', - label : 'Standard', - panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_HD.mp4' }, - }, - { - id : 'SD', - label : 'Low', - panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_SD.mp4' }, - }, - ] - }] - ], + container: 'viewer', + adapter: [PhotoSphereViewer.EquirectangularVideoAdapter, { + muted: true, + }], + caption: 'Ayutthaya © meetle', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + navbar: 'video caption settings fullscreen', + + plugins: [ + PhotoSphereViewer.VideoPlugin, + PhotoSphereViewer.SettingsPlugin, + [PhotoSphereViewer.ResolutionPlugin, { + defaultResolution: 'HD', + resolutions: [ + { + id: 'UHD', + label: 'Ultra high', + panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_UHD.mp4' }, + }, + { + id: 'FHD', + label: 'High', + panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_FHD.mp4' }, + }, + { + id: 'HD', + label: 'Standard', + panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_HD.mp4' }, + }, + { + id: 'SD', + label: 'Low', + panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_SD.mp4' }, + }, + ], + }], + ], }); ``` ::: - ## Configuration #### `autoplay` -- type: `boolean` -- default: `false` + +- type: `boolean` +- default: `false` Automatically starts the video on load. #### `muted` -- type: `boolean` -- default: `false` (`true` if `autoplay=true`) + +- type: `boolean` +- default: `false` Mute the video by default. #### `resolution` -- type: `number` -- default: `64` + +- type: `number` +- default: `64` The number of faces of the sphere geometry used to display the panorama, higher values can reduce deformations on straight lines at the cost of performances. _Note: the actual number of faces is `resolution² / 2`._ - ## Panorama options -When using this adapter the `panorama` option and the `setPanorama()` method accept an object to configure the video. +When using this adapter, the `panorama` option and the `setPanorama()` method accept an object to configure the video. #### `source` (required) -- type: `string` + +- type: `string` Path of the video file. The video must not be larger than 4096 pixels or it won't be displayed on handled devices. diff --git a/docs/guide/adapters/equirectangular.md b/docs/guide/adapters/equirectangular.md index f9dc88171..9e0a791c5 100644 --- a/docs/guide/adapters/equirectangular.md +++ b/docs/guide/adapters/equirectangular.md @@ -1,6 +1,8 @@ # Equirectangular -> [Equirectangular projection](https://en.wikipedia.org/wiki/Equirectangular_projection) is one of the simplest way to create the texture of a sphere. It is the default projection used by most 360° cameras. +::: module +[Equirectangular projection](https://en.wikipedia.org/wiki/Equirectangular_projection) is one of the simplest way to create the texture of a sphere. It is the default projection used by most 360° cameras. +::: ::: tip There is no need to declare the equirectangular adapter as it is the default one, unless you want to change its configuration. @@ -8,57 +10,58 @@ There is no need to declare the equirectangular adapter as it is the default one ```js const viewer = new PhotoSphereViewer.Viewer({ - adapter: [PhotoSphereViewer.EquirectangularAdapter, { - resolution: 64, // default - }], - panorama: 'path/panorama.jpg', + adapter: [PhotoSphereViewer.EquirectangularAdapter, { + resolution: 64, // default + }], + panorama: 'path/panorama.jpg', }); ``` - ## Configuration #### `resolution` -- type: `number` -- default: `64` -The number of faces of the sphere geometry used to display the panorama, higher values can reduce deformations on straight lines at the cost of performances. +- type: `number` +- default: `64` -_Note: the actual number of faces is `resolution² / 2`._ +The number of faces of the sphere geometry used to display the panorama, higher values can reduce deformations on straight lines at the cost of performances. +_Note: the actual number of faces is `resolution² / 2`._ ## Cropped panorama **Photo Sphere Viewer** supports cropped panorama given the appropriate configuration is provided. Cropped panoramas are not covering the whole 360°×180° sphere area but only a smaller portion. For example you might have a image covering 360° horizontally but only 90° vertically, or a semi sphere (180°×180°) These incomplete panoramas are handled in two ways by Photo Sphere viewer: - - Read XMP metadata directly from the file with `useXmpData` option (this is the default) - - Provide the `panoData` configuration object/function + +- Read XMP metadata directly from the file with `useXmpData` option (this is the default) +- Provide the `panoData` configuration object/function Use the [Playground](#playground) at the bottom of this page to find the best values for your panorama. ### Theory In both case the data contains six important values: - - Full panorama width - - Full panorama height - - Cropped area width - - Cropped area height - - Cropped area left - - Cropped area right + +- Full panorama width +- Full panorama height +- Cropped area width +- Cropped area height +- Cropped area left +- Cropped area right The `Full panorama width` / `Full panorama height` ratio must always be 2:1. `Cropped area width` and `Cropped area height` are the actual size of your image. `Cropped area left` and `Cropped area right` are used to define the cropped area position. The data can also contains angular values: - - Pose Heading - - Pose Pitch - - Pose Roll + +- Pose Heading +- Pose Pitch +- Pose Roll ![XMP_pano_pixels](../../images/XMP_pano_pixels.png) More information on [Google documentation](https://developers.google.com/streetview/spherical-metadata). - ### Provide cropping data #### With XMP @@ -100,23 +103,22 @@ You can also directly pass the values to Photo Sphere Viewer with the `panoData` ```js const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: 'path/to/panorama.jpg', - panoData: { - fullWidth: 6000, - fullHeight: 3000, - croppedWidth: 4000, - croppedHeight: 2000, - croppedX: 1000, - croppedY: 500, - poseHeading: 0, // 0 to 360 - posePitch: 0, // -90 to 90 - poseRoll: 0, // -180 to 180 - } + container: 'viewer', + panorama: 'path/to/panorama.jpg', + panoData: { + fullWidth: 6000, + fullHeight: 3000, + croppedWidth: 4000, + croppedHeight: 2000, + croppedX: 1000, + croppedY: 500, + poseHeading: 0, // 0 to 360 + posePitch: 0, // -90 to 90 + poseRoll: 0, // -180 to 180 + }, }); ``` - ### Playground Use this demo to find the best values for your image. diff --git a/docs/guide/adapters/little-planet.md b/docs/guide/adapters/little-planet.md index f6304a9b4..a077d2b0f 100644 --- a/docs/guide/adapters/little-planet.md +++ b/docs/guide/adapters/little-planet.md @@ -1,43 +1,44 @@ -# Little planet +# Little planet -> Displays an [equirectangular](equirectangular.md) panorama with a little planet effect. +::: module +Displays an [equirectangular](equirectangular.md) panorama with a little planet effect. -This adapter is available in the core `photo-sphere-viewer` package in `dist/adapters/little-planet.js`. +This adapter is available in the `@photo-sphere-viewer/little-planet-adapter` package. +::: -::: warning Compatibility +::: warning This adapter is not complatible with some options and plugins, it is provided as it just for fun. ::: ```js const viewer = new PhotoSphereViewer.Viewer({ - adapter: PhotoSphereViewer.LittlePlanetAdapter, - panorama: 'path/panorama.jpg', + adapter: PhotoSphereViewer.LittlePlanetAdapter, + panorama: 'path/panorama.jpg', }); ``` - ## Example ::: code-demo ```yaml title: PSV Little Planet Demo -resources: - - path: adapters/little-planet.js - imports: LittlePlanetAdapter +packages: + - name: little-planet-adapter + imports: LittlePlanetAdapter ``` ```js const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - adapter: PhotoSphereViewer.LittlePlanetAdapter, - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, + container: 'viewer', + adapter: PhotoSphereViewer.LittlePlanetAdapter, + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, }); ``` diff --git a/docs/guide/components/README.md b/docs/guide/components/README.md index ccae74796..b3a067a8e 100644 --- a/docs/guide/components/README.md +++ b/docs/guide/components/README.md @@ -3,7 +3,8 @@ You can easily reuse Photo Sphere Viewer internal components from your custom navbar buttons, plugins, or wherever in your application. The reusables components are: -- [panel](panel.md): display HTML content on a sidebar on the left of the viewer -- [notification](notification.md): display a small message above the navbar -- [overlay](overlay.md): display a message with an illustration on top of the viewer -- [tooltip](tooltip.md): add custom tooltips over the viewer + +- [panel](panel.md): display HTML content on a sidebar on the left of the viewer +- [notification](notification.md): display a small message above the navbar +- [overlay](overlay.md): display a message with an illustration on top of the viewer +- [tooltip](tooltip.md): add custom tooltips over the viewer diff --git a/docs/guide/components/notification.md b/docs/guide/components/notification.md index ba9a2e5b3..f8bcd5d1e 100644 --- a/docs/guide/components/notification.md +++ b/docs/guide/components/notification.md @@ -1,9 +1,8 @@ # Notification - - -> Display a small message above the navbar. - +::: module classes/Core.Notification.html" +Display a small message above the navbar. +::: ## Example @@ -19,40 +18,34 @@ title: PSV Notification Demo const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - navbar: [ - 'zoom', - 'caption', - 'fullscreen', - ], + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + navbar: ['zoom', 'caption', 'fullscreen'], }); let i = 1; -setInterval(function() { - viewer.notification.show({ - content: `Annoying notification #${i++}`, - timeout: 1000, - }); +setInterval(() => { + viewer.notification.show({ + content: `Annoying notification #${i++}`, + timeout: 1000, + }); }, 2000); - ``` ::: - ## Methods ### `show(config)` Show the notification. -| option | type | | -|---|---|---| +| option | type | | +| ------ | ---- | - | | `id` | `string` | Unique identifier of the notification, this will be used to `hide` the notification only if the content has not been replaced by something else. | | `content` (required) | `string` | HTML content of the notification. | | `timeout` | `number` | Auto-hide delay in milliseconds. | @@ -65,7 +58,6 @@ Hide the notification, without condition if `id` is not provided, or only if the Check if the notification is visible. - ## Events ### `show-notification(id)` diff --git a/docs/guide/components/overlay.md b/docs/guide/components/overlay.md index 63199e9aa..4bf21a513 100644 --- a/docs/guide/components/overlay.md +++ b/docs/guide/components/overlay.md @@ -1,9 +1,8 @@ # Overlay - - -> Display a message with an illustration on top of the viewer. - +::: module classes/Core.Overlay.html +Display a message with an illustration on top of the viewer. +::: ## Example @@ -19,31 +18,31 @@ title: PSV Overlay Demo const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, }); function showOverlay() { - viewer.overlay.show({ - image: document.getElementById('overlay-icon').innerHTML, - text: 'Lorem ipsum dolor sit amet', - dissmisable: true, - }); + viewer.overlay.show({ + image: document.getElementById('overlay-icon').innerHTML, + title: 'Lorem ipsum dolor sit amet', + dissmisable: true, + }); } -viewer.once('ready', showOverlay); +viewer.addEventListener('ready', showOverlay, { once: true }); ``` ```html ``` @@ -55,11 +54,11 @@ viewer.once('ready', showOverlay); Show the overlay. -| option | type | | -|---|---|---| +| option | type | | +| ------ | ---- | - | | `id` | `string` | Unique identifier of the overlay, this will be used to `hide` the overlay only if the content has not been replaced by something else. | -| `text` (required) | `string` | Main message of the overlay. | -| `subtext` | `string` | Secondary message of the overlay. | +| `title` (required) | `string` | Main message of the overlay. | +| `text` | `string` | Secondary message of the overlay. | | `image` | `string` | SVG icon or image displayed above the text. | ### `hide([id])` @@ -70,7 +69,6 @@ Hide the overlay, without condition if `id` is not provided, or only if the last Check if the overlay is visible. - ## Events ### `show-overlay(id)` diff --git a/docs/guide/components/panel.md b/docs/guide/components/panel.md index 5e62c4523..12b16241d 100644 --- a/docs/guide/components/panel.md +++ b/docs/guide/components/panel.md @@ -1,9 +1,8 @@ # Panel - - -> Display HTML content on a sidebar on the left of the viewer. - +::: module classes/Core.Panel.html +Display HTML content on a sidebar on the left of the viewer. +::: ## Example @@ -17,92 +16,91 @@ title: PSV Panel Demo ```js const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; -const buttonId = 'panel-button'; -const panelId = 'custom-panel'; +const BUTTON_ID = 'panel-button'; +const PANEL_ID = 'custom-panel'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - navbar: [ - 'zoom', - { - id: buttonId, - title: 'Toggle panel', - content: '🆘', - onClick: function() { - if (viewer.panel.isVisible(panelId)) { - viewer.panel.hide(); - } else { - viewer.panel.show({ - id: panelId, - width: '60%', - content: document.querySelector('#panel-content').innerHTML, - }); - } - }, - }, - 'caption', - 'fullscreen', - ], + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + navbar: [ + 'zoom', + { + id: BUTTON_ID, + title: 'Toggle panel', + content: '🆘', + onClick: () => { + if (viewer.panel.isVisible(PANEL_ID)) { + viewer.panel.hide(); + } else { + viewer.panel.show({ + id: PANEL_ID, + width: '60%', + content: document.querySelector('#panel-content').innerHTML, + }); + } + }, + }, + 'caption', + 'fullscreen', + ], }); -viewer.on('open-panel', function(e, id) { - if (id === panelId) { - viewer.navbar.getButton(buttonId).toggleActive(true); - } +viewer.addEventListener('show-panel', ({ panelId }) => { + if (panelId === PANEL_ID) { + viewer.navbar.getButton(BUTTON_ID).toggleActive(true); + } }); -viewer.on('close-panel', function(e, id) { - if (id === panelId) { - viewer.navbar.getButton(buttonId).toggleActive(false); - } +viewer.addEventListener('hide-panel', ({ panelId }) => { + if (panelId === PANEL_ID) { + viewer.navbar.getButton(BUTTON_ID).toggleActive(false); + } }); ``` ```html ``` ::: - ## Methods ### `show(config)` Open the side panel. -| option | type | | -|---|---|---| +| option | type | | +| ------ | ---- | - | | `id` | `string` | Unique identifier of the panel, this will be used to `hide` the panel only if the content has not been replaced by something else. It will be used to store the width defined by the user when using the resize handle. | | `content` (required) | `string` | HTML content of the panel. | | `noMargin` (default `false`) | `boolean` | Remove the default margins inside the panel. | -| `width` (default `400px`) | `string` | Initial width if the panel (example: `100%`, `600px`). | -| `clickHandler` | `function` | Function called when the user clicks inside the panel or presses the Enter key while an element focused. | +| `width` (default `400px`) | `string` | Initial width of the panel (example: `100%`, `600px`). | +| `clickHandler(target)` | `function` | Function called when the user clicks inside the panel or presses the Enter key while an element focused. | ::: tip Content focus After openning, the first focusable element (`a`, `button` or anything with `tabindex`) will be focused, allowing the user to navigate with the Tab key and activate the `clickHandler` with the `Enter` key. @@ -116,13 +114,12 @@ Hide the panel, without condition if `id` is not provided, or only if the last ` Check if the panel is opened. - ## Events -### `open-panel(id)` +### `show-panel(id)` -Triggered when the panel is opened. +Triggered when the panel is shown. -### `close-panel(id)` +### `hide-panel(id)` -Triggered when the panel is closed. +Triggered when the panel is hidden. diff --git a/docs/guide/components/tooltip.md b/docs/guide/components/tooltip.md index a6ebafa22..70d513637 100644 --- a/docs/guide/components/tooltip.md +++ b/docs/guide/components/tooltip.md @@ -1,11 +1,10 @@ # Tooltip - - -> Add custom tooltips over the viewer. - -To add a tooltip you must call `viewer.tooltip.create()`, this will a return a tooltip instance with two methods : `move()` and `hide()`. This allows to have multiple tooltips at the same time. +::: module classes/Core.Tooltip.html +Add custom tooltips over the viewer. +::: +To add a tooltip you must call `viewer.createTooltip()`, this will a return a tooltip instance with two methods : `move()` and `hide()`. This allows to have multiple tooltips at the same time. ## Example @@ -21,59 +20,58 @@ title: PSV Tooltip Demo const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - navbar: 'zoom caption fullscreen', + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + navbar: 'zoom caption fullscreen', }); let tooltip; function onMouseMove(e) { - if (!tooltip) { - tooltip = viewer.tooltip.create({ - content: '© Damien Sorel', - left: e.clientX, - top: e.clientY, - position: 'top right', - }); - } else { - tooltip.move({ - left: e.clientX, - top: e.clientY, - position: 'top right', - }); - } + if (!tooltip) { + tooltip = viewer.createTooltip({ + content: '© Damien Sorel', + left: e.clientX, + top: e.clientY, + position: 'top right', + }); + } else { + tooltip.move({ + left: e.clientX, + top: e.clientY, + position: 'top right', + }); + } } function onMouseLeave() { - if (tooltip) { - tooltip.hide(); - tooltip = null; - } + if (tooltip) { + tooltip.hide(); + tooltip = null; + } } -viewer.once('ready', function() { - viewer.parent.addEventListener('mousemove', onMouseMove); - viewer.parent.addEventListener('mouseleave', onMouseLeave); -}); +viewer.addEventListener('ready', () => { + viewer.parent.addEventListener('mousemove', onMouseMove); + viewer.parent.addEventListener('mouseleave', onMouseLeave); +}, { once: true }); ``` ::: - ## Methods ### `create(config)` Create a tooltip. -| option | type | | -|---|---|---| -| `content` (required) | `string` |HTML content of the tooltip. | +| option | type | | +| ------ | ---- | - | +| `content` (required) | `string` | HTML content of the tooltip. | | `top` & `left` (required) | `number` | Pixel coordinates of the tooltip relative to the top-left corner of the viewer. | | `position` (default `top center`) | `string` | Tooltip position toward it's arrow tip. Accepted values are combinations of `top`, `center`, `bottom` and `left`, `center`, `right`. | | `className` | `string` | Additional CSS class added to the tooltip. | @@ -87,7 +85,6 @@ Updates the position of the tooltip, the parameters are the same `top`, `left` a Hide and destroy the tooltip. - ## Events ### `show-tooltip(data)` diff --git a/docs/guide/config.md b/docs/guide/config.md index 8ae995d3e..30a727e8b 100644 --- a/docs/guide/config.md +++ b/docs/guide/config.md @@ -7,83 +7,95 @@ Photo Sphere Viewer uses a lot of angles for its configuration, most of them can ::: ::: tip Positions definitions -Some methods takes positionnal arguments, this is either on combination `longitude` and `latitude` (radians or degrees, **note:** those are local coordinates and not related to GPS) or `x` and `y` properties (corresponding to the pixel position on the source panorama file). +Some methods takes positionnal arguments, this is either on combination `yaw` and `pitch` (radians or degrees) or `textureX` and `textureY` properties (corresponding to the pixel position on the source panorama file). ::: ## Standard options #### `container` (required) -- type: `HTMLElement | string` + +- type: `HTMLElement | string` HTML element which will contain the panorama, or identifier of the element. ```js -container: document.querySelector('.viewer') +container: document.querySelector('.viewer'); -container: 'viewer' // will target [id="viewer"] +container: 'viewer'; // will target [id="viewer"] ``` #### `panorama` (required) -- type: `*` + +- type: `*` Path to the panorama. Must be a single URL for the default equirectangular adapter. Other adapters support other values. #### `adapter` -- default: `equirectangular` + +- default: `equirectangular` Which [adapter](./adapters) used to load the panorama. #### `plugins` -- type: `array` + +- type: `array` List of enabled [plugins](../plugins/README.md). #### `caption` -- type: `string` + +- type: `string` A text displayed in the navbar. If the navbar is disabled, the caption won't be visible. HTML is allowed. #### `description` -- type: `string` + +- type: `string` A text displayed in the side panel when the user clicks the "i" button. HTML is allowed. #### `downloadUrl` -- type: `string` -- default: `=panorama` for equirectangular panoramas + +- type: `string` +- default: `=panorama` for equirectangular panoramas Define the file which will be downloaded with the `download` button. This is particularly useful for adapters that use multiple files, like the CubemapAdapter or the EquirectangularTilesAdapter. #### `size` -- type: `{ width: integer, height: integer }` -The final size of the panorama container. By default the size of `container` is used and is followed during window resizes. +- type: `{ width: integer, height: integer }` + +The final size of the panorama container. By default the size of `container` is used and is followed when resized. #### `navbar` Configuration of the [navbar](./navbar.md). #### `minFov` -- type: `integer` -- default: `30` -Minimal field of view (corresponds to max zoom), between 1 and 179. +- type: `integer` +- default: `30` + +Minimal field of view (maximum zoom), between 1 and `maxFov`. #### `maxFov` -- type: `integer` -- default: `90` -Maximal field of view (corresponds to min zoom), between 1 and 179. +- type: `integer` +- default: `90` + +Maximal field of view (minimum zoom), between `minFov` and 180. #### `defaultZoomLvl` -- type: `integer` -- default: `50` + +- type: `integer` +- default: `50` Initial zoom level, between 0 (for `maxFov`) and 100 (for `minfov`). #### `fisheye` -- type: `boolean | double` -- default: `false` + +- type: `boolean | double` +- default: `false` Enable fisheye effect with `true` or specify the effect strength (`true` = `1.0`). @@ -91,127 +103,109 @@ Enable fisheye effect with `true` or specify the effect strength (`true` = `1.0` This mode can have side-effects on markers rendering and some adapters. ::: -#### `defaultLong` -- type: `double | string` -- default: `0` - -Initial longitude, between 0 and 2π. - -#### `defaultLat` -- type: `double | string` -- default: `0` - -Initial latitude, between -π/2 and π/2. +#### `defaultYaw` -#### `autorotateDelay` -- type: `integer` -- default: `null` +- type: `double | string` +- default: `0` -Delay after which the automatic rotation will begin, in milliseconds. +Initial horizontal angle, between 0 and 2π. -#### `autorotateIdle` -- type: `boolean` -- default: `false` +#### `defaultPitch` -Restarts the automatic rotation if the user is idle for `autorotateDelay`. +- type: `double | string` +- default: `0` -**Note:** the rotation won't restart of the user explicitly clicks on the navbar button. +Initial vertical angle, between -π/2 and π/2. -#### `autorotateSpeed` -- type: `string` -- default: `2rpm` - -Speed of the automatic rotation. Can be a negative value to reverse the rotation. - -#### `autorotateLat` -- type: `double | string` -- default: `defaultLat` - -Latitude at which the automatic rotation is performed. - -#### `autorotateZoomLvl` -- type: `number` -- default: `null` +#### `lang` -Zoom level at which the automatic rotation is performed. If `null` the current zoom is kept. +- type: `object` +- default: -#### `lang` -- type: `object` -- default: ```js lang: { - autorotate: 'Automatic rotation', - zoom : 'Zoom', - zoomOut : 'Zoom out', - zoomIn : 'Zoom in', - move : 'Move', - download : 'Download', + zoom: 'Zoom', + zoomOut: 'Zoom out', + zoomIn: 'Zoom in', + moveUp: 'Move up', + moveDown: 'Move down', + moveLeft: 'Move left', + moveRight: 'Move right', + download: 'Download', fullscreen: 'Fullscreen', - menu : 'Menu', + menu: 'Menu', twoFingers: 'Use two fingers to navigate', - ctrlZoom : 'Use ctrl + scroll to zoom the image', - loadError : 'The panorama can\'t be loaded', + ctrlZoom: 'Use ctrl + scroll to zoom the image', + loadError: 'The panorama can\'t be loaded', } ``` Various texts used in the viewer. #### `loadingImg` -- type: `string` -Path to an image displayed in the center of the loading circle. +- type: `string` + +Path to an image displayed in the center of the loader. #### `loadingTxt` -- type: `string` -- default: `'Loading...'` -Text displayed in the center of the loading circle, only used if `loadingImg` is not provided. +- type: `string` +- default: `'Loading...'` + +Text displayed in the center of the loader, only used if `loadingImg` is not provided. #### `mousewheel` -- type: `boolean` -- default: `true` + +- type: `boolean` +- default: `true` Enables zoom with the mouse wheel. #### `mousemove` -- type: `boolean` -- default: `true` + +- type: `boolean` +- default: `true` Enables panorama rotation with mouse click+move or with a finger swipe on touch screens. #### `mousewheelCtrlKey` -- type: `boolean` -- default: `false` + +- type: `boolean` +- default: `false` Requires to use the ctrl key to zoom the panorama. This allows to scroll on the page without interfering with the viewer. If enabled, an overlay asking the user to use ctrl + scroll is displayed when ctrl key is not pressed. #### `touchmoveTwoFingers` -- type: `boolean` -- default: `false` -Requires two fingers to rotate the panorama. This allows standard touch-scroll navigation in the page containing the viewer. If enabled, an overlay asking the user to use two fingers is displayed when only one touch is detected. +- type: `boolean` +- default: `false` +Requires two fingers to rotate the panorama. This allows standard touch-scroll navigation in the page containing the viewer. If enabled, an overlay asking the user to use two fingers is displayed when only one finger is detected. ## Advanced options -#### `overlay` -- type: `*` +#### `overlay` + +- type: `*` Path to an additional transparent panorama which will be displayed on top of the main one. The overlay can also be changed with the `setOverlay()` method or within the options of the `setPanorama()` method. -::: warning Adapters +::: warning Only the default [equirectangular](./adapters/equirectangular.md) and the [cubemap](./adapters/cubemap.md) adapters support this feature. ::: -#### `overlayOpacity` -- type: `number` -- default: `1` +#### `overlayOpacity` + +- type: `number` +- default: `1` Opacity of the `overlay`. #### `sphereCorrection` -- type: `{ pan: double, tilt: double, roll: double }` -- default: `{ pan:0, tilt:0, roll: 0 }` + +- type: `{ pan: double, tilt: double, roll: double }` +- default: `{ pan:0, tilt:0, roll: 0 }` Allows to rotate the panorama sphere. Angles are in radians. @@ -220,27 +214,31 @@ Allows to rotate the panorama sphere. Angles are in radians. ![pan-tilt-toll](../images/pan-tilt-roll.png) #### `moveSpeed` -- type: `double` -- default `1` + +- type: `double` +- default `1` Speed multiplicator for panorama moves. Used for click move, touch move and navbar buttons. #### `zoomSpeed` -- type: `double` -- default `1` + +- type: `double` +- default `1` Speed multiplicator for panorama zooms. Used for mouse wheel, touch pinch and navbar buttons. #### `useXmpData` -- type: `boolean` -- default `true` + +- type: `boolean` +- default `true` Read real image size from XMP data, must be kept `true` if the panorama has been cropped after shot. This is used for [cropped panorama](./adapters/equirectangular.md#cropped-panorama). #### `panoData` -- type: `object | function` -Overrides XMP data found in the panorama file (or simply defines it if `useXmpData=false`). +- type: `object | function` + +Overrides XMP data found in the panorama file (or if `useXmpData=false`). All parameters are optional. ```js @@ -261,23 +259,24 @@ It can also be a function to dynamically compute the cropping config depending o ```js panoData: (image) => ({ - fullWidth : image.width, - fullHeight : Math.round(image.width / 2), - croppedWidth : image.width, - croppedHeight: image.height, - croppedX : 0, - croppedY : Math.round((image.width / 2 - image.height) / 2), -}) + fullWidth: image.width, + fullHeight: Math.round(image.width / 2), + croppedWidth: image.width, + croppedHeight: image.height, + croppedX: 0, + croppedY: Math.round((image.width / 2 - image.height) / 2), +}); ``` **Note:** if the XMP data and/or `panoData` contains heading/pitch/roll data, they will be applied before `sphereCorrection`. ::: warning -Only the default `equirectangular` adapter and low-resolution panorama of `equirectangular-tiles` supports `panoData`, for other adapters you can only use [`sphereCorrection`](#spherecorrection) if the tilt/roll/pan needs to be corrected. +Only the default [equirectangular](./adapters/equirectangular.md) adapter and low-resolution panorama of [equirectangular-tiles](./adapters/equirectangular-tiles.md) supports `panoData`, for other adapters you can only use [`sphereCorrection`](#spherecorrection) if the tilt/roll/pan needs to be corrected. ::: #### `requestHeaders` -- type: `object | function` + +- type: `object | function` Sets the HTTP headers when loading the images files. @@ -291,42 +290,46 @@ It can also be a function to dynamically set the request headers before every ca ```js requestHeaders: (url) => ({ - header: value, -}) + header: value, +}); ``` #### `canvasBackground` -- type: `string` -- default: `#000` + +- type: `string` +- default: `#000` Background of the canvas, which will be visible when using cropped panoramas. #### `moveInertia` -- type: `boolean` -- default: `true` + +- type: `boolean` +- default: `true` Enabled smooth animation after a manual move. #### `withCredentials` -- type: `boolean` -- default: `false` + +- type: `boolean` +- default: `false` Use credentials for HTTP requests. #### `keyboard` -- type: `boolean | object` -- default: + +- type: `boolean | object` +- default: + ```js keyboard: { - 'ArrowUp': 'rotateLatitudeUp', - 'ArrowDown': 'rotateLatitudeDown', - 'ArrowRight': 'rotateLongitudeRight', - 'ArrowLeft': 'rotateLongitudeLeft', - 'PageUp': 'zoomIn', - 'PageDown': 'zoomOut', - '+': 'zoomIn', - '-': 'zoomOut', - ' ': 'toggleAutorotate', + 'ArrowUp': 'ROTATE_UP', + 'ArrowDown': 'ROTATE_DOWN', + 'ArrowRight': 'ROTATE_RIGHT', + 'ArrowLeft': 'ROTATE_LEFT', + 'PageUp': 'ZOOM_IN', + 'PageDown': 'ZOOM_OUT', + '+': 'ZOOM_IN', + '-': 'ZOOM_OUT', } ``` diff --git a/docs/guide/events.md b/docs/guide/events.md index c6723623d..45684a036 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -4,9 +4,28 @@ ## Presentation -Photo Sphere Viewer uses [uEvent API](https://github.com/mistic100/uEvent#uevent). The full list of events is available on the . +Photo Sphere Viewer objects (`Viewer` and plugins) all implement the [EventTarget API](https://developer.mozilla.org/docs/Web/API/EventTarget) to dispatch events. It also implements a custom TypeScript interface which allows events to be strongly typed. -Event listeners take an `Event` object as first parameter, this object is generally not used. Other parameters are available after this event object. +Event listeners are called with a single `Event` subclass which has additional properties. Notably : + +- `type` is the name of the event +- `target` is a reference to the viewer (or plugin) itself + +```js +import { events } from '@photo-sphere-viewer/core'; + +// use a constant (prefered) +viewer.addEventListener(events.PositionUpdateEvent.type, (e) => { + // e.type === 'position-updated' + // e.target === viewer + // e.position +}); + +// or a magic value +viewer.addEventListener('position-updated', ({ position }) => ()); +``` + +The full list of events is available in the . ## Main events @@ -14,11 +33,11 @@ This section describes the most useful events available. ### `click(data)` | `dblclick(data)` -Triggered when the user clicks on the viewer (excluding the navbar and the side panel), it contains many information about where the user clicked including a [marker](../plugins/plugin-markers.md) if the `clickEventOnMarker` option is enabled. +Triggered when the user clicks on the viewer (excluding the navbar and the side panel), it contains many information about where the user clicked including a [marker](../plugins/markers.md) if the `clickEventOnMarker` option is enabled. ```js -viewer.on('click', (e, data) => { - console.log(`${data.rightclick?'right ':''}clicked at longitude: ${data.longitude} latitude: ${data.latitude}`); +viewer.addEventListener('click', ({ data }) => { + console.log(`${data.rightclick ? 'right ' : ''}clicked at yaw: ${data.yaw} pitch: ${data.pitch}`); }); ``` @@ -26,30 +45,30 @@ A `click` event is always fired before a `dblclick`. ### `position-updated(position)` -Triggered when the view longitude and/or latitude changes. +Triggered when the view yaw and/or pitch change. ```js -viewer.on('position-updated', (e, position) => { - console.log(`new position is longitude: ${position.longitude} latitude: ${position.latitude}`); +viewer.addEventListener('position-updated', ({ position }) => { + console.log(`new position is yaw: ${position.yaw} pitch: ${position.pitch}`); }); ``` ### `ready` -Triggered when the panorama image has been loaded and the viewer is ready to perform the first render. +Triggered once when the panorama image has been loaded and the viewer is ready to perform the first render. ```js -viewer.once('ready', () => { +viewer.addEventListener('ready', () => { console.log(`viewer is ready`); -}); +}, { once: true }); ``` -### `zoom-updated(level)` +### `zoom-updated(zoomLevel)` Triggered when the zoom level changes. ```js -viewer.on('zoom-updated', (e, level) => { - console.log(`new zoom level is ${level}`); +viewer.addEventListener('zoom-updated', ({ zoomLevel }) => { + console.log(`new zoom level is ${zoomLevel}`); }); ``` diff --git a/docs/guide/frameworks.md b/docs/guide/frameworks.md index be6a80d2f..70c585a46 100644 --- a/docs/guide/frameworks.md +++ b/docs/guide/frameworks.md @@ -2,4 +2,4 @@ The following framework implementations made by the community are available. -- [`react-photo-sphere-viewer`](https://www.npmjs.com/package/react-photo-sphere-viewer) by Elia Lazzari +- [`react-photo-sphere-viewer`](https://www.npmjs.com/package/react-photo-sphere-viewer) by Elia Lazzari diff --git a/docs/guide/methods.md b/docs/guide/methods.md index 4bff46a1d..6b3e84394 100644 --- a/docs/guide/methods.md +++ b/docs/guide/methods.md @@ -4,26 +4,23 @@ ## Presentation -Many methods are available to control the viewer from your application. The full list of methods is available on the . +Many methods are available to control the viewer from your application. The full list of methods is available in the . ::: tip Modular architecture Photo Sphere Viewer is internally splitted in multiple components, this has an impact on where are located each method. For example, the methods to control the navbar are in the `navbar` object. -The important components are : - - `navbar` - - `hud` - - `panel` +Read more about [reusable components](./components/). ::: It is good practice to wait for the `ready` event before calling any method. ```js -viewer.once('ready', () => { +viewer.addEventListener('ready', () => { viewer.rotate({ - x: 1500, - y: 1000 + textureX: 1500, + textureY: 1000 }); -}); +}, { once: true }); ``` ## Main methods @@ -32,12 +29,12 @@ This section describes the most useful methods available. ### `animate(options): Animation` -Rotate and zoom the view with a smooth animation. You can change the position (`longitude`, `latitude` or `x`, `y`) and the zoom level (`zoom`). The `speed` option is either a duration in milliseconds or a string containing the speed in revolutions per minute (`2rpm`). It returns a `PSV.Animation` which is a standard Promise with an additional `cancel` method. +Rotate and zoom the view with a smooth animation. You can change the position (`yaw`, `pitch` or `textureX`, `textureY`) and the zoom level (`zoom`). The `speed` option is either a duration in milliseconds or a string containing the speed in revolutions per minute (`2rpm`). It returns a `Animation` object which is a standard Promise with an additional `cancel` method. ```js viewer.animate({ - longitude: Math.PI / 2, - latitude: '20deg', + yaw: Math.PI / 2, + pitch: '20deg', zoom: 50, speed: '2rpm', }) @@ -61,16 +58,16 @@ Return the current zoom level between 0 and 100. Immediately rotate the view without animation. ```js -// you can also use longitude and latitude +// you can also use yaw and pitch viewer.rotate({ - x: 1500, - y: 600, + textureX: 1500, + textureY: 600, }); ``` ### `setOption(option, value)` -Update an option of the viewer. Some options cannot be changed : `panorama`, `panoData`, `container`, `adapter` and `plugins`. +Update an option of the viewer. Some options cannot be changed : `panorama`, `panoData`, `container`, `overlay`, `overlayOpacity`, `adapter` and `plugins`. ```js viewer.setOption('fisheye', true); @@ -82,14 +79,13 @@ Update multiple options at once. ```js viewer.setOptions({ - fisheye: true, - autorotateSpeed: '-1rpm', + fisheye: true, }); ``` ### `setPanorama(panorama[, options]): Promise` -Change the panorama image with an optional transition animation (enabled by default). See all options on the . +Change the panorama image with an optional transition animation (enabled by default). See all options in the . ```js viewer.setPanorama('image.jpg') diff --git a/docs/guide/migration-v3.md b/docs/guide/migration-v3.md deleted file mode 100644 index 691a63fa8..000000000 --- a/docs/guide/migration-v3.md +++ /dev/null @@ -1,95 +0,0 @@ -# Migration from v3 - -[[toc]] - -> Photo Sphere Viewer 4 is a complete rewrite of the internals of the library using ES6 modules and a modular plugin system. The library is still compatible with less modern browsers and any build systems (or no build system at all) but many methods and options have been moved and renamed. - - -## Plugins - -The following features have been moved into separated plugins : - - markers : [MarkersPlugin](../plugins/plugin-markers.md) - - gyroscope support : [GyroscopePlugin](../plugins/plugin-gyroscope.md) - - stereo view : [StereoPlugin](../plugins/plugin-stereo.md) - - longitude and latitude ranges : [VisibleRangePlugin](../plugins/plugin-visible-range.md) - - -## Options - -### Renamed options - -The viewer configuration is globally the same as version 3 but **every snake_case properties are now in camelCase**, for example `default_lat` is now named `defaultLat`. -Be sure to rename your configuration properties, the old naming is not supported at all. - -#### Other renamed options : - -- `usexmpdata` → `useXmpData` -- `anim_speed` → `autorotateSpeed` -- `anim_lat` → `autorotateLat` -- `time_anim` → `autorotateDelay` -- `default_fov`→ `defaultZoomLvl` -- `mousemove_hover` → removed - -### Deleted options - -- `transition` → use `transition` and `showLoader` options of `setPanorama()` -- `tooltip` → the properties of the tooltip are now extracted from the stylesheet -- `webgl` → WebGL is now always enabled since three.js deprecated the CanvasRenderer -- `panorama_roll` → use `sphereCorrection` with the `roll` property -- `mousewheel_factor` - - -## Methods - -### Moved methods - -In version 3, all methods where on the main `PhotoSphereViewer` object. Now in version 4, many methods have been moved to sub-objects for the renderer, hud, left-panel, etc. -Bellow is the mapping of the most common methods, please check the for a complete list of methods. - -#### General - -- `render()` → `needsUpdate()` (prefered) or `renderer.render()` -- `preloadPanorama()` → `textureLoader.preloadPanorama()` -- `clearPanoramaCache()` → removed, use `THREE.Cache.clear()` -- `getPanoramaCache()` → removed, use `THREE.Cache.get()` - -#### Navbar - -- `showNavbar()` → `navbar.show()` -- `hideNavbar()` → `navbar.hide()` -- `toggleNavbar()` → `navbar.toggle()` -- `getNavbarButton()` → `navbar.getButton()` -- `setCaption()` → `navbar.setCaption()` - -#### Notification - -- `showNotification()` → `notification.show()` -- `hideNotification()` → `notification.hide()` -- `isNotificationVisible()` → `notification.isVisible()` - -#### Overlay - -- `showOverlay()` → `overlay.show()` -- `hideOverlay()` → `overlay.hide()` -- `isOverlayVisible()` → `overlay.isVisible()` - -#### Panel - -- `showPanel()` → `panel.show()` -- `hidePanel()` → `panel.hide()` - -#### Tooltip - -- `showTooltip()` → `tooltip.show()` -- `hideTooltip()` → `tooltip.hide()` -- `isTooltipVisible()` → `tooltip.isVisible()` - - -## Events - -Although the events are now triggered by the new sub-objects, you only have to use `on()` and `off()` on the main object, for the sake of simplicity. - - -## Markers - -Markers' properties are not directly stored in the `Marker` object anymore but in its `config` attribute. You should only use `markersPlugin.updateMarker()` to update these properties. diff --git a/docs/guide/migration.md b/docs/guide/migration.md new file mode 100644 index 000000000..68b9bd361 --- /dev/null +++ b/docs/guide/migration.md @@ -0,0 +1,103 @@ +# Migration from v4 + +[[toc]] + +This page is here to help you to migrate from Photo Sphere Viewer 4 to Photo Sphere Viewer 5. + +## Packages, ESM and ES6 + +The previous `photo-sphere-viewer` package has been splitted in to multiple packages. `@photo-sphere-viewer/core` contains the core functionnality (mainly the `Viewer` class) and other packages contain plugins and adapters. + +`@photo-sphere-viewer` packages use modern ES6 syntax, which is supported by all major browsers. This means you will need a transpiler like Babel if you want to support oldest browsers. + +Each package contains the following files : + +- **index.js** : UMD bundle +- **index.module.js** : ESM bundle +- **index.d.ts** : TypeScript declaration +- **index.css** (optional) : stylesheet +- **index.scss** (optional) : SASS source + +Here is the full list of packages you might need : + +- @photo-sphere-viewer/core +- @photo-sphere-viewer/cubemap-adapter +- @photo-sphere-viewer/cubemap-tiles-adapter +- @photo-sphere-viewer/cubemap-video-adapter +- @photo-sphere-viewer/equirectangular-tiles-adapter +- @photo-sphere-viewer/equirectangular-video-adapter +- @photo-sphere-viewer/little-planet-adapter +- @photo-sphere-viewer/autorotate-plugin +- @photo-sphere-viewer/compass-plugin +- @photo-sphere-viewer/gallery-plugin +- @photo-sphere-viewer/gyroscope-plugin +- @photo-sphere-viewer/markers-plugin +- @photo-sphere-viewer/resolution-plugin +- @photo-sphere-viewer/settings-plugin +- @photo-sphere-viewer/stereo-plugin +- @photo-sphere-viewer/video-plugin +- @photo-sphere-viewer/virtual-tour-plugin +- @photo-sphere-viewer/visible-range-plugin + +## Options + +### Positions + +Photo Sphere Viewer uses two coordinates systems : spherical (longitude + latitude) and pixels on the source image (x + y). Theses options have been renamed to avoid confusion with GPS system. + +- `longitude` → `yaw` +- `latitude` → `pitch` +- `x` → `textureX` +- `y` → `textureY` + +### Renamed options + +- `defaultLong` → `defaultYaw` +- `defaultLat` → `defaultPitch` + +### Renamed markers + +- `polygonRad` → `polygon` +- `polygonPx` → `polygonPixels` +- `polylineRad` → `polyline` +- `polylinePx` → `polylinePixels` + +## Automatic rotation + +All the automatic rotation features have been moved to [a new plugin](../plugins/autorotate.md). The `autorotateXxx` options have been removed. + +## Events + +For this version, Photo Sphere Viewer dropped uEvent library to rely exclusively on the native events system. + +This means you will have to update all your usage of `on()` and `off()` methods. Let's see that with an example. + +:::: tabs + +::: tab Before + +```js +viewer.on('position-updated', (e, position) => { + console.log(position.longitude); +}); + +viewer.off('position-updated'); +``` + +::: + +::: tab After + +```js +const handler = ({ position }) => { + console.log(position.yaw); +}; + +viewer.addEventListener('position-updated', handler); + +viewer.removeEventListener('position-updated', handler); +``` + +::: + +:::: diff --git a/docs/guide/navbar.md b/docs/guide/navbar.md index 47e5a2a15..bb6d2c4c8 100644 --- a/docs/guide/navbar.md +++ b/docs/guide/navbar.md @@ -6,20 +6,19 @@ The `navbar` option is an array which can contain the following elements: - - `autorotate` - - `zoomOut` - - `zoomRange` - - `zoomIn` - - `zoom` = `zoomOut` + `zoomRange` + `zoomIn` - - `moveLeft` - - `moveRight` - - `moveTop` - - `moveDown` - - `move` = `moveLeft` + `moveRight` + `moveTop` + `moveDown` - - `download` - - `description` - - `caption` - - `fullscreen` +- `zoomOut` +- `zoomRange` +- `zoomIn` +- `zoom` = `zoomOut` + `zoomRange` + `zoomIn` +- `moveLeft` +- `moveRight` +- `moveTop` +- `moveDown` +- `move` = `moveLeft` + `moveRight` + `moveTop` + `moveDown` +- `download` +- `description` +- `caption` +- `fullscreen` ## Plugins buttons @@ -30,39 +29,46 @@ Some [plugins](../plugins/) add new buttons to the navbar and will be automatica You can also add as many custom buttons you want. A custom button is an object with the following options: #### `content` (required) -- type : `string` + +- type : `string` Content of the button. Preferably a square image or SVG icon. #### `onClick(viewer)` (required) -- type : `function(Viewer)` + +- type : `function(Viewer)` Function called when the button is clicked. #### `id` -- type : `string` + +- type : `string` Unique identifier of the button, usefull when using the `navbar.getButton()` method. #### `title` -- type : `string` + +- type : `string` Tooltip displayed when the mouse is over the button. #### `className` -- type : `string` + +- type : `string` CSS class added to the button. #### `disabled` -- type : `boolean` -- default : `false` + +- type : `boolean` +- default : `false` Initially disable the button. #### `visible` -- type : `boolean` -- default : `true` + +- type : `boolean` +- default : `true` Initially show the button. @@ -72,27 +78,25 @@ The API allows to change the visibility of the button at any time: viewer.navbar.getButton('my-button').show(); ``` - ## Example This example uses some core buttons, the caption and a custom button. ```js new PhotoSphereViewer.Viewer({ - navbar: [ - 'autorotate', - 'zoom', - { - id: 'my-button', - content: '', - title: 'Hello world', - className: 'custom-button', - onClick: (viewer) => { - alert('Hello from custom button'); - }, - }, - 'caption', - 'fullscreen', - ], + navbar: [ + 'zoom', + { + id: 'my-button', + content: '', + title: 'Hello world', + className: 'custom-button', + onClick: (viewer) => { + alert('Hello from custom button'); + }, + }, + 'caption', + 'fullscreen', + ], }); ``` diff --git a/docs/guide/style.md b/docs/guide/style.md index 9a8992eba..125b4fb5c 100644 --- a/docs/guide/style.md +++ b/docs/guide/style.md @@ -4,126 +4,129 @@ Photo Sphere Viewer comes with a default darkish theme. You can customize it by ```scss // overrides -$psv-loader-color: rgba(0, 0, 0, 0.5); +$psv-loader-color: rgba(0, 0, 0, .5); $psv-loader-width: 100px; .... // main stylesheet -@import '~photo-sphere-viewer/src/styles/index.scss'; +@import '~@photo-sphere-viewer/core/index.scss'; // plugins stylesheets -@import '~photo-sphere-viewer/src/plugins/markers/style.scss'; -@import '~photo-sphere-viewer/src/plugins/virtual-tour/style.scss'; +@import '~@photo-sphere-viewer/markers-plugin/index.scss'; +@import '~@photo-sphere-viewer/virtual-tour-plugin/index.scss'; .... ``` ## Global -| variable | default | description | -|---|---|---| -| $psv-main-background | radial-gradient(...) | Background of the viewer, visible when no panorama is set | -| $psv-element-focus-outline | 2px solid #007cff | Outline applied to focusable elements (navbar, panel, etc.) | +| variable | default | description | +| -------------------------- | -------------------- | ----------------------------------------------------------- | +| $psv-main-background | radial-gradient(...) | Background of the viewer, visible when no panorama is set | +| $psv-element-focus-outline | 2px solid #007cff | Outline applied to focusable elements (navbar, panel, etc.) | ## Loader -| variable | default | description | -|---|---|---| -| $psv-loader-color | rgba(61, 61, 61, .7) | Color of the loader bar | -| $psv-loader-width | 150px | Size of the loader | -| $psv-loader-tickness | 10px | Thickness of the loader bar | -| $psv-loader-font | 14px sans-serif | Font of the loading text | +| variable | default | description | +| -------------------- | ----------------------- | -------------------------------- | +| $psv-loader-color | rgba(255, 255, 255, .7) | Color of the loader bar and text | +| $psv-loader-bg-color | rgba(61, 61, 61, .5) | Color of the loader background | +| $psv-loader-width | 150px | Size of the loader | +| $psv-loader-tickness | 10px | Thickness of the loader bar | +| $psv-loader-border | 3px | Inner border of the loader | +| $psv-loader-font | 600 16px sans-serif | Font of the loading text | ## Navbar -| variable | default | description | -|---|---|---| -| $psv-navbar-height | 40px | Height of the navbar | -| $psv-navbar-background | rgba(61, 61, 61, .5) | Background color of the navbar | -| $psv-caption-font | 16px sans-serif | Font of the caption | -| $psv-caption-color | rgba(255, 255, 255, .7) | Text color of the caption | +| variable | default | description | +| ----------------------- | ----------------------- | ------------------------------ | +| $psv-navbar-height | 40px | Height of the navbar | +| $psv-navbar-background | rgba(61, 61, 61, .5) | Background color of the navbar | +| $psv-caption-font | 16px sans-serif | Font of the caption | +| $psv-caption-text-color | rgba(255, 255, 255, .7) | Text color of the caption | #### Buttons -| variable | default | description | -|---|---|---| -| $psv-buttons-height | 20px | Inner height of the buttons | -| $psv-buttons-color | rgba(255, 255, 255, .7 | Icon color of the buttons | -| $psv-buttons-background | transparent | Background color of the buttons | +| variable | default | description | +| ------------------------------ | ----------------------- | ------------------------------------------- | +| $psv-buttons-height | 20px | Inner height of the buttons | +| $psv-buttons-color | rgba(255, 255, 255, .7) | Icon color of the buttons | +| $psv-buttons-background | transparent | Background color of the buttons | | $psv-buttons-active-background | rgba(255, 255, 255, .2) | Background color of the buttons when active | -| $psv-buttons-disabled-opacity | .5 | Opacity of disabled buttons | -| $psv-buttons-hover-scale | 1.2 | Scale applied to buttons on mouse hover | -| $psv-buttons-hover-scale-delay | .2s | Duration of the scale animation | +| $psv-buttons-disabled-opacity | .5 | Opacity of disabled buttons | +| $psv-buttons-hover-scale | 1.2 | Scale applied to buttons on mouse hover | +| $psv-buttons-hover-scale-delay | 200ms | Duration of the scale animation | #### Zoom range -| variable | default | description | -|---|---|---| -| $psv-zoom-range-width | 80px | Size of the zoom range | -| $psv-zoom-range-tickness | 1px | Tickness of the zoom range | -| $psv-zoom-disk-diameter | 7px | Size of the zoom handle | -| $psv-zoom-range-media-min-width | 600px | Hides the zoom range on small screens | - +| variable | default | description | +| ------------------------------- | ------- | ------------------------------------- | +| $psv-zoom-range-width | 80px | Size of the zoom range | +| $psv-zoom-range-tickness | 1px | Tickness of the zoom range | +| $psv-zoom-range-diameter | 7px | Size of the zoom handle | +| $psv-zoom-range-media-min-width | 600px | Hides the zoom range on small screens | ## Tooltip -| variable | default | description | -|---|---|---| -| $psv-tooltip-background-color | rgba(61, 61, 61, .8) | Background color of tooltips | -| $psv-tooltip-radius | 4px | Border radius of the tooltips | -| $psv-tooltip-padding | .5em 1em | Content padding of the tooltips | -| $psv-tooltip-arrow-size | 7px | Tooltips' arrow size | -| $psv-tooltip-max-width | 200px | Maximum width of the tooltips' content | -| $psv-tooltip-text-color | rgb(255, 255, 255) | Text color of the tooltips | -| $psv-tooltip-font | 14px sans-serif | Font of the tooltips | -| $psv-tooltip-text-shadow | 0 1px #000 | Shadow applied to the tooltips' text | -| $psv-tooltip-shadow-color | rgba(90, 90, 90, .7) | Color of the tooltips' shadow | -| $psv-tooltip-shadow-offset | 3px | Size of the tooltips' shadow | -| $psv-tooltip-animate-offset | 5px | Distance travelled on show animation | -| $psv-tooltip-animate-delay | 0.1s | Duration of the show animation | +| variable | default | description | +| --------------------------- | ----------------------- | -------------------------------------- | +| $psv-tooltip-background | rgba(61, 61, 61, .8) | Background of tooltips | +| $psv-tooltip-radius | 4px | Border radius of the tooltips | +| $psv-tooltip-padding | .5em 1em | Content padding of the tooltips | +| $psv-tooltip-arrow-size | 7px | Tooltips' arrow size | +| $psv-tooltip-arrow-color | $psv-tooltip-background | Color of the arrow | +| $psv-tooltip-max-width | 200px | Maximum width of the tooltips' content | +| $psv-tooltip-text-color | rgb(255, 255, 255) | Text color of the tooltips | +| $psv-tooltip-font | 14px sans-serif | Font of the tooltips | +| $psv-tooltip-text-shadow | 0 1px #000 | Shadow applied to the tooltips' text | +| $psv-tooltip-shadow-color | rgba(90, 90, 90, .7) | Color of the tooltips' shadow | +| $psv-tooltip-shadow-offset | 3px | Size of the tooltips' shadow | +| $psv-tooltip-animate-offset | 5px | Distance travelled on show animation | +| $psv-tooltip-animate-delay | 100ms | Duration of the show animation | ## Panel -| variable | default | description | -|---|---|---| -| $psv-panel-background | rgba(10, 10, 10, .7) | Background of the panel | -| $psv-panel-width | 400px | Default width of the panel | -| $psv-panel-padding | 1em | Content padding of the panel | -| $psv-panel-text-color | rgb(220, 220, 220) | Default text color of the panel | -| $psv-panel-font | 16px sans-serif | Default font of the panel | +| variable | default | description | +| ------------------------ | -------------------- | ------------------------------- | +| $psv-panel-background | rgba(10, 10, 10, .7) | Background of the panel | +| $psv-panel-width | 400px | Default width of the panel | +| $psv-panel-padding | 1em | Content padding of the panel | +| $psv-panel-text-color | rgb(220, 220, 220) | Default text color of the panel | +| $psv-panel-font | 16px sans-serif | Default font of the panel | +| $psv-panel-animate-delay | 100ms | Duration of the show animation | #### Menu -| variable | default | description | -|---|---|---| -| $psv-panel-title-font | 24px sans-serif | Font of the menu title | -| $psv-panel-title-icon-size | 24px | Size of the menu title icon | -| $psv-panel-title-margin | 24px | Margin of the menu title | -| $psv-panel-menu-item-height | 1.5em | Minimum eight of an item in the menu | -| $psv-panel-menu-item-padding | .5em 1em | Padding of an item in the menu | -| $psv-panel-menu-odd-background | rgba(255, 255, 255, .1) | Background color of odd items in the menu | -| $psv-panel-menu-even-background | transparent | Background color of even items in the menu | -| $psv-panel-menu-hover-background | rgba(255, 255, 255, .2) | Background color of items on mouse hover | +| variable | default | description | +| -------------------------------- | ----------------------- | ------------------------------------------ | +| $psv-panel-title-font | 24px sans-serif | Font of the menu title | +| $psv-panel-title-icon-size | 24px | Size of the menu title icon | +| $psv-panel-title-margin | 24px | Margin of the menu title | +| $psv-panel-menu-item-height | 1.5em | Minimum eight of an item in the menu | +| $psv-panel-menu-item-padding | .5em 1em | Padding of an item in the menu | +| $psv-panel-menu-odd-background | rgba(255, 255, 255, .1) | Background color of odd items in the menu | +| $psv-panel-menu-even-background | transparent | Background color of even items in the menu | +| $psv-panel-menu-hover-background | rgba(255, 255, 255, .2) | Background color of items on mouse hover | ## Notification -| variable | default | description | -|---|---|---| -| $psv-notification-position-from | -$psv-navbar-height | Position of the notification when hidden | -| $psv-notification-position-to | $psv-navbar-height * 2 | Position of the notification when visible | -| $psv-notification-animate-delay | 0.2s | Duration of the show animation | -| $psv-notification-background-color | $psv-tooltip-background-color | Background color of the notification | -| $psv-notification-radius | $psv-tooltip-radius | Border radius of the notification | -| $psv-notification-padding | $psv-tooltip-padding | Content padding of the notification | -| $psv-notification-font | $psv-tooltip-font | Font of the notification | -| $psv-notification-text-color | $psv-tooltip-text-color | Text color of the notification | +| variable | default | description | +| ------------------------------- | ----------------------- | ----------------------------------------- | +| $psv-notification-position-from | -$psv-navbar-height | Position of the notification when hidden | +| $psv-notification-position-to | $psv-navbar-height \* 2 | Position of the notification when visible | +| $psv-notification-animate-delay | 200ms | Duration of the show animation | +| $psv-notification-background | $psv-tooltip-background | Background color of the notification | +| $psv-notification-radius | $psv-tooltip-radius | Border radius of the notification | +| $psv-notification-padding | $psv-tooltip-padding | Content padding of the notification | +| $psv-notification-font | $psv-tooltip-font | Font of the notification | +| $psv-notification-text-color | $psv-tooltip-text-color | Text color of the notification | ## Overlay -| variable | default | description | -|---|---|---| -| $psv-overlay-color | black | Text color of the overlay | -| $psv-overlay-opacity | .8 | Opacity of the overlay | -| $psv-overlay-font-family | sans-serif | Default font of the overlay | -| $psv-overlay-text-size | 30px | Main text size in the overlay | -| $psv-overlay-subtext-size | 20px | Secondary text size in the overlay | -| $psv-overlay-image-size | (portrait: 50vw,
landscape: 25vw) | Image/Icon size, depending on screen orientation | +| variable | default | description | +| ------------------------ | ------------------------------------ | ------------------------------------------------ | +| $psv-overlay-opacity | .8 | Opacity of the overlay | +| $psv-overlay-title-font | 30px sans-serif | Font of the overlay title | +| $psv-overlay-title-color | black | Color of the overlay title | +| $psv-overlay-text-font | 20px sans-serif | Font of the overlay text | +| $psv-overlay-text-color | rgba(0, 0, 0, .8) | Color of the overlay text | +| $psv-overlay-image-size | (portrait: 50vw,
landscape: 25vw) | Image/Icon size, depending on screen orientation | diff --git a/docs/images/demos/autorotate.gif b/docs/images/demos/autorotate.gif deleted file mode 100644 index 2c98224b6..000000000 Binary files a/docs/images/demos/autorotate.gif and /dev/null differ diff --git a/docs/plugins/README.md b/docs/plugins/README.md index c3558bd8f..4d7da2acf 100644 --- a/docs/plugins/README.md +++ b/docs/plugins/README.md @@ -4,46 +4,48 @@ Plugins are used to add new functionalities to Photo Sphere Viewer. They can acc ## Import official plugins -Official plugins (listed on the left menu) are available in the the main `photo-sphere-viewer` package inside the `dist/plugins` directory. Some plugins also have an additional CSS file. +Official plugins (listed on the left menu) are available in various `@photo-sphere-viewer/***-plugin` packages. Some plugins also have an additional CSS file. **Example for the Markers plugin:** :::: tabs ::: tab Direct import + ```html - + - + ``` + ::: ::: tab ES import -Import `photo-sphere-viewer/dist/plugins/markers.css` with the prefered way depending on your tooling. +Import `@photo-sphere-viewer/markers-plugin/index.css` with the prefered way depending on your tooling. ```js -import { MarkersPlugin } from 'photo-sphere-viewer/dist/plugins/markers'; +import { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin'; ``` + ::: :::: - ## Using a plugin All plugins consists of a JavaScript class which must be provided to the `plugins` array. Some plugins will also take a configuration object provided in a nested array. ```js const viewer = new PhotoSphereViewer.Viewer({ - plugins: [ - PhotoSphereViewer.GyroscopePlugin, - [PhotoSphereViewer.MarkersPlugin, { - option1: 'foo', - option2: 'bar', - }], - ], + plugins: [ + PhotoSphereViewer.GyroscopePlugin, + [PhotoSphereViewer.MarkersPlugin, { + option1: 'foo', + option2: 'bar', + }], + ], }); ``` @@ -54,7 +56,7 @@ const markersPlugin = viewer.getPlugin(PhotoSphereViewer.MarkersPlugin); markersPlugin.addMarker(/* ... */); -markersPlugin.on('select-marker', () => { - /* ... */ +markersPlugin.addEventListener('select-marker', () => { + /* ... */ }); ``` diff --git a/docs/plugins/autorotate.md b/docs/plugins/autorotate.md new file mode 100644 index 000000000..0fa8dc7c3 --- /dev/null +++ b/docs/plugins/autorotate.md @@ -0,0 +1,272 @@ +# AutorotatePlugin + +::: module modules/plugin__Autorotate.html +Adds a an automatic rotation the panorama, which starts automatically on idle or with a click on a button. The rotation can also be configured to visit specific points. + +This plugin is available in the `@photo-sphere-viewer/autorotate-plugin` package. +::: + +[[toc]] + +## Usage + +:::: tabs + +::: tab Standard + +In standard mode the panorama will simply rotate around, you can configure the `autorotatePitch` and `autorotateZoomLvl`. + +```js +const viewer = new PhotoSphereViewer.Viewer({ + plugins: [ + [PhotoSphereViewer.AutorotatePlugin, { + autorotatePitch: '5deg', + }], + ], +}); +``` +::: + +::: tab Keypoints + +In keypoints mode the plugin is configured with a list of `keypoints` which can be either a position object (either `yaw`/`pitch` or `textureX`/`textureY`) or the identifier of an existing [marker](./markers.md). + +It is also possible to configure each keypoint with a pause time and a tooltip. + +```js +const viewer = new PhotoSphereViewer.Viewer({ + plugins: [ + [PhotoSphereViewer.AutorotatePlugin, { + keypoints: [ + 'existing-marker-id', + + { longitude: Math.PI / 2, latitude: 0 }, + + { + position: { yaw: Math.PI, pitch: Math.PI / 6 }, + pause: 5000, + tooltip: 'This is interesting', + }, + + { + markerId: 'another-marker', // will use the marker tooltip if any + pause: 2500, + }, + ], + }], + ], +}); +``` +::: + +:::: + +## Example + +### Standard + +::: code-demo + +```yaml +title: PSV Autorotate Demo +packages: + - name: autorotate-plugin + imports: AutorotatePlugin +``` + +```js +const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; + +const viewer = new PhotoSphereViewer.Viewer({ + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + + plugins: [ + [PhotoSphereViewer.AutorotatePlugin, { + autorotatePitch: '5deg', + }], + ], +}); +``` + +::: + +### Keypoints + +::: code-demo + +```yaml +title: PSV Autorotate Keypoints Demo +packages: + - name: autorotate-plugin + imports: AutorotatePlugin + - name: markers-plugin + imports: MarkersPlugin + style: true +``` + +```js +const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; + +const viewer = new PhotoSphereViewer.Viewer({ + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + + navbar: [ + 'autorotate', + 'zoom', + { + // a custom button to change keypoints + title: 'Change points', + content: '🔄', + onClick: randomPoints, + }, + 'caption', + 'fullscreen', + ], + + plugins: [ + [PhotoSphereViewer.AutorotatePlugin, { + autostartDelay: 1000, + autorotateSpeed: '3rpm', + }], + PhotoSphereViewer.MarkersPlugin, + ], +}); + +const autorotatePlugin = viewer.getPlugin(PhotoSphereViewer.AutorotatePlugin); +const markersPlugin = viewer.getPlugin(PhotoSphereViewer.MarkersPlugin); + +viewer.addEventListener('ready', randomPoints, { once: true }); + +/** + * Randomize the keypoints and add corresponding markers + */ +function randomPoints() { + const points = []; + + for (let i = 0, l = Math.random() * 2 + 4; i < l; i++) { + points.push({ + position: { + yaw: ((i + Math.random()) * 2 * Math.PI) / l, + pitch: (Math.random() * Math.PI) / 3 - Math.PI / 6, + }, + pause: i % 3 === 0 ? 2000 : 0, + tooltip: 'Test tooltip', + }); + } + + markersPlugin.setMarkers( + points.map((pt, i) => { + return { + id: '#' + i, + position: pt.position, + image: baseUrl + 'pictos/pin-red.png', + size: { width: 32, height: 32 }, + anchor: 'bottom center', + }; + }) + ); + + autorotatePlugin.setKeypoints(points); +} +``` + +::: + +## Configuration + +#### `autostartDelay` + +- type: `integer` +- default: `2000` + +Delay after which the automatic rotation will begin, in milliseconds. + +#### `autostartOnIdle` + +- type: `boolean` +- default: `true` + +Restarts the automatic rotation if the user is idle for `autostartDelay`. + +**Note:** the rotation won't restart of the user explicitly clicks on the navbar button. + +#### `autorotateSpeed` + +- type: `string` +- default: `2rpm` + +Speed of the automatic rotation. Can be a negative value to reverse the rotation. + +#### `autorotatePitch` + +- type: `double | string` +- default: `defaultPitch` + +Vertical angle at which the automatic rotation is performed. + +#### `autorotateZoomLvl` + +- type: `number` +- default: `null` + +Zoom level at which the automatic rotation is performed. If `null` the current zoom is kept. + +#### `keypoints` + +- type: `AutorotateKeypoint[]` + +Initial keypoints, does the same thing as calling `setKeypoints()` just after initialisation. + +#### `startFromClosest` + +- type: `boolean` +- default: `true` + +Start from the closest keypoint instead of the first keypoint of the array. + +#### `lang` + +- type: `object` +- default: + +```js +lang: { + autorotate: 'Automatic rotation', +} +``` + +_Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ + +## Methods + +#### `setKeypoints(keypoints)` + +Changes or remove the keypoints. + +#### `start()` / `stop()` / `toggle()` + +As it says. + +## Events + +#### `autorotate(autorotateEnabled)` + +Triggered when the automatic rotation is enabled/disabled. + +## Buttons + +This plugin adds buttons to the default navbar: + +- `autorotate` allows to toggle the rotation on and off + +If you use a [custom navbar](../guide/navbar.md) you will need to manually add the buttons to the list. diff --git a/docs/plugins/compass.md b/docs/plugins/compass.md new file mode 100644 index 000000000..a78cb57a2 --- /dev/null +++ b/docs/plugins/compass.md @@ -0,0 +1,208 @@ +# CompassPlugin + +::: module modules/plugin__Compass.html +Adds a compass on the viewer to represent which portion of the sphere is currently visible. + +This plugin is available in the `@photo-sphere-viewer/compass-plugin` package. **It has a stylesheet.** +::: + +[[toc]] + +## Usage + +The plugin can be configured with a list of `hotspots` which are small dots on the compass. It can also display markers positions. + +```js +const viewer = new PhotoSphereViewer.Viewer({ + plugins: [ + [PhotoSphereViewer.CompassPlugin, { + hotspots: [ + { yaw: '45deg' }, + { yaw: '60deg', color: 'red' }, + ], + }], + ], +}); +``` + +## Example + +::: code-demo + +```yaml +title: PSV Compass Demo +packages: + - name: compass-plugin + imports: CompassPlugin + style: true + - name: markers-plugin + imports: MarkersPlugin + style: true +``` + +```js +const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; + +const viewer = new PhotoSphereViewer.Viewer({ + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + + plugins: [ + [PhotoSphereViewer.CompassPlugin, { + hotspots: [ + { yaw: '0deg' }, + { yaw: '90deg' }, + { yaw: '180deg' }, + { yaw: '270deg' }, + ], + }], + [PhotoSphereViewer.MarkersPlugin, { + markers: [ + { + id: 'pin', + position: { yaw: 0.11, pitch: 0.32 }, + image: baseUrl + 'pictos/pin-blue.png', + size: { width: 32, height: 32 }, + anchor: 'bottom center', + data: { compass: '#304ACC' }, + }, + { + id: 'polygon', + polygonPixels: [ + 2941, 1413, 3042, 1402, 3222, 1419, 3433, 1463, + 3480, 1505, 3438, 1538, 3241, 1543, 3041, 1555, + 2854, 1559, 2739, 1516, 2775, 1469, 2941, 1413, + ], + svgStyle: { + fill: 'rgba(255,0,0,0.2)', + stroke: 'rgba(255, 0, 50, 0.8)', + strokeWidth: '2px', + }, + data: { compass: 'rgba(255, 0, 50, 0.8)' }, + }, + { + id: 'polyline', + polylinePixels: [ + 2478, 1635, 2184, 1747, 1674, 1953, 1166, 1852, + 709, 1669, 301, 1519, 94, 1399, 34, 1356, + ], + svgStyle: { + stroke: 'rgba(80, 150, 50, 0.8)', + strokeLinecap: 'round', + strokeLinejoin: 'round', + strokeWidth: '20px', + }, + data: { compass: 'rgba(80, 150, 50, 0.8)' }, + }, + ], + }], + ], +}); +``` + +::: + +::: tip +The north is always at yaw=0, if you need to change where is the north you can use `panoData.poseHeading` or `sphereCorrection.pan` option. +::: + +## Configuration + +#### `size` + +- type: `string` +- default: `'120px'` + +The size of the compass, can be declared in `px`, `rem`, `vh`, etc. + +#### `position` + +- type: `string` +- default: `'top left'` + +Accepted positions are combinations of `top`, `center`, `bottom` and `left`, `center`, `right`. + +#### `navigation` + +- type: `boolean` +- default: `true` + +Allows to click on the compass to rotate the viewer. + +#### `hotspots` + +- type: `Hotspot[]` +- default: `null` + +Small dots visible on the compass. Each spot consist of a position (either `yaw`/`pitch` or `textureX`/`textureY`) and an optional `color` which overrides the global `hotspotColor`. + +::: tip +[Markers](./markers.md) can be displayed on the compass by defining their `compass` data, which can be `true` or a specific color. + +```js +markers: [ + { + id: 'marker-1', + image: 'pin.png', + position: { yaw: '15deg', pitch: 0 }, + data: { compass: true }, + }, + { + id: 'marker-2', + text: 'Warning', + position: { yaw: '-45deg', pitch: 0 }, + data: { compass: 'orange' }, + }, +]; +``` + +::: + +#### `backgroundSvg` + +- type: `string` +- default: SVG provided by the plugin + +SVG used as background of the compass (must be square). + +#### `coneColor` + +- type: `string` +- default: `'rgba(255, 255, 255, 0.2)'` + +Color of the cone of the compass. + +#### `navigationColor` + +- type: `string` +- default: `'rgba(255, 0, 0, 0.2)'` + +Color of the navigation cone. + +#### `hotspotColor` + +- type: `string` +- default: `'rgba(0, 0, 0, 0.5)'` + +Default color of hotspots. + +## Methods + +#### `setHotspots(hotspots)` + +Changes the hotspots. + +```js +compassPlugin.setHotspots([ + { yaw: '0deg' }, + { yaw: '10deg', color: 'red' }, +]); +``` + +#### `clearHotspots()` + +Removes all hotspots diff --git a/docs/plugins/gallery.md b/docs/plugins/gallery.md new file mode 100644 index 000000000..a32438279 --- /dev/null +++ b/docs/plugins/gallery.md @@ -0,0 +1,190 @@ +# GalleryPlugin + +::: module modules/plugin__Gallery.html +Adds a gallery on the bottom of the viewer to navigate between multiple panoramas. + +This plugin is available in the `@photo-sphere-viewer/gallery-plugin` package. **It has a stylesheet.** +::: + +[[toc]] + +::: warning +GalleryPlugin is not compatible with ResolutionPlugin. +::: + +## Usage + +The plugin has a list of `items`, each configuring the corresponding panorama, a name and a thumbnail. + +```js +const viewer = new PhotoSphereViewer.Viewer({ + plugins: [ + [PhotoSphereViewer.GalleryPlugin, { + items: [ + { + id: 'pano-1', + name: 'Panorama 1', + panorama: 'path/to/pano-1.jpg', + thumbnail: 'path/to/pano-1-thumb.jpg', + }, + { + id: 'pano-2', + name: 'Panorama 2', + panorama: 'path/to/pano-2.jpg', + thumbnail: 'path/to/pano-2-thumb.jpg', + }, + ], + }], + ], +}); +``` + +## Example + +::: code-demo + +```yaml +title: PSV Gallery Demo +packages: + - name: gallery-plugin + imports: GalleryPlugin + style: true +``` + +```js +const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; + +const viewer = new PhotoSphereViewer.Viewer({ + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + + plugins: [ + [PhotoSphereViewer.GalleryPlugin, { + visibleOnLoad: true, + }], + ], +}); + +const gallery = viewer.getPlugin(PhotoSphereViewer.GalleryPlugin); + +gallery.setItems([ + { + id: 'sphere', + panorama: baseUrl + 'sphere.jpg', + thumbnail: baseUrl + 'sphere-small.jpg', + options: { + caption: 'Parc national du Mercantour © Damien Sorel', + }, + }, + { + id: 'sphere-test', + panorama: baseUrl + 'sphere-test.jpg', + name: 'Test sphere', + }, + { + id: 'key-biscayne', + panorama: baseUrl + 'tour/key-biscayne-1.jpg', + thumbnail: baseUrl + 'tour/key-biscayne-1-thumb.jpg', + name: 'Key Biscayne', + options: { + caption: 'Cape Florida Light, Key Biscayne © Pixexid', + }, + }, +]); +``` + +::: + +## Configuration + +#### `items` + +- type: `array` +- default: `GalleryItem[]` + +The list of items, see bellow. + +#### `visibleOnLoad` + +- type: `boolean` +- default: `false` + +Displays the gallery when loading the first panorama. The user will be able to toggle the gallery with the navbar button. + +#### `hideOnClick` + +- type: `boolean` +- default: `true + +Hides the gallery when the user clicks on an item. + +#### `thumbnailSize` + +- type: `{ width: number, height: number }` +- default: `{ width: 200, height: 100 }` + +Size of the thumbnails. + +#### `lang` + +- type: `object` +- default: + +```js +lang: { + gallery: 'Gallery', +} +``` + +_Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ + +### Items + +#### `id` (required) + +- type: `number|string` + +Unique identifier of the item. + +#### `thumbnail` (recommended) + +- type: `string` +- default: `''` + +URL of the thumbnail. + +#### `name` + +- type: `string` +- default: `''` + +Text visible over the thumbnail. + +#### `panorama` (required) + +Refer to the main [config page](../guide/config.md#panorama-required). + +#### `options` + +- type: `PanoramaOptions` +- default: `null` + +Any option supported by the [setPanorama()](../guide/methods.md#setpanorama-panorama-options-promise) method. + +## Methods + +#### `setItems(items)` + +Changes the list of items. + +## Buttons + +This plugin adds buttons to the default navbar: + +- `gallery` allows to toggle the gallery panel + +If you use a [custom navbar](../guide/navbar.md) you will need to manually add the buttons to the list. diff --git a/docs/plugins/plugin-gyroscope.md b/docs/plugins/gyroscope.md similarity index 67% rename from docs/plugins/plugin-gyroscope.md rename to docs/plugins/gyroscope.md index 7778680d9..7ba75d3a0 100644 --- a/docs/plugins/plugin-gyroscope.md +++ b/docs/plugins/gyroscope.md @@ -1,11 +1,10 @@ # GyroscopePlugin - - -> Adds gyroscope controls on mobile devices. - -This plugin is available in the core `photo-sphere-viewer` package in `dist/plugins/gyroscope.js`. +::: module modules/plugin__Gyroscope.html +Adds gyroscope controls on mobile devices. +This plugin is available in the `@photo-sphere-viewer/gyroscope-plugin` package. +::: ## Usage @@ -13,52 +12,58 @@ Once enabled the plugin will add a new "Gyroscope" button only shown when the gy ```js const viewer = new PhotoSphereViewer.Viewer({ - plugins: [ - PhotoSphereViewer.GyroscopePlugin, - ], + plugins: [PhotoSphereViewer.GyroscopePlugin], }); ``` +::: tip +The gyroscope API only works on HTTPS domains. +::: + ::: warning There are known inconsistencies of orientation data accross devices. If the panorama is not displayed in the expected orientation, this plugin is not faulty. ::: - ## Configuration #### `touchmove` -- type: `boolean` -- default: `true` + +- type: `boolean` +- default: `true` Allows to pan horizontally the camera when the gyroscope is enabled (requires global `mousemove=true`). #### `absolutePosition` -- type: `boolean` -- default: `false` + +- type: `boolean` +- default: `false` By default the camera will keep its current horizontal position when the gyroscope is enabled. Turn this option `true` to enable absolute positionning and only use the device orientation. #### `moveMode` -- type: `smooth` | `fast` -- default: `smooth` + +- type: `smooth` | `fast` +- default: `smooth` How the gyroscope data is used to rotate the panorama. #### `lang` -- type: `object` -- default: + +- type: `object` +- default: + ```js lang: { - gyroscope : 'Gyroscope', + gyroscope: 'Gyroscope', } ``` _Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ - ## Buttons This plugin adds buttons to the default navbar: -- `gyroscope` allows to toggle the gyroscope control + +- `gyroscope` allows to toggle the gyroscope control If you use a [custom navbar](../guide/navbar.md) you will need to manually add the buttons to the list. diff --git a/docs/plugins/markers.md b/docs/plugins/markers.md new file mode 100644 index 000000000..b6f269fc5 --- /dev/null +++ b/docs/plugins/markers.md @@ -0,0 +1,631 @@ +# MarkersPlugin + +::: module modules/plugin__Markers.html +Displays various shapes, images and texts on the viewer. + +This plugin is available in the `@photo-sphere-viewer/markers-plugin` package. **It has a stylesheet.** +::: + +[[toc]] + +## Usage + +The plugin provides a powerful markers system allowing to define points of interest on the panorama with optional tooltip and description. Markers can be dynamically added/removed and you can react to user click/tap. + +There are four types of markers : + +- **HTML** defined with the `html` attribute +- **Images** defined with the `image`/`imageLayer` attribute +- **SVGs** defined with the `square`/`rect`/`circle`/`ellipse`/`path` attribute +- **Dynamic polygons & polylines** defined with the `polygon`/`polygonPixels`/`polyline`/`polylinePixels` attribute + +Markers can be added at startup with the `markers` option or after load with the various methods. + +```js +const viewer = new PhotoSphereViewer.Viewer({ + plugins: [ + [PhotoSphereViewer.MarkersPlugin, { + markers: [ + { + id: 'new-marker', + position: { yaw: '45deg', pitch: '0deg' }, + image: 'assets/pin-red.png', + size: { width: 32, height: 32 }, + }, + ], + }], + ], +}); + +const markersPlugin = viewer.getPlugin(PhotoSphereViewer.MarkersPlugin); + +markersPlugin.addEventListener('select-marker', ({ marker }) => { + markersPlugin.updateMarker({ + id: marker.id, + image: 'assets/pin-blue.png', + }); +}); +``` + +## Example + +The following example contains all types of markers. Click anywhere on the panorama to add a red marker, right-click to change it's color and double-click to remove it. + +::: code-demo + +```yaml +title: PSV Markers Demo +packages: + - name: markers-plugin + imports: MarkersPlugin + style: true +``` + +```js +const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; + +const viewer = new PhotoSphereViewer.Viewer({ + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + + plugins: [ + [PhotoSphereViewer.MarkersPlugin, { + // list of markers + markers: [ + { + // image marker that opens the panel when clicked + id: 'image', + position: { yaw: 0.32, pitch: 0.11 }, + image: baseUrl + 'pictos/pin-blue.png', + size: { width: 32, height: 32 }, + anchor: 'bottom center', + zoomLvl: 100, + tooltip: 'A image marker. Click me!', + content: document.getElementById('lorem-content').innerHTML, + }, + { + // image marker rendered in the 3D scene + id: 'imageLayer', + imageLayer: baseUrl + 'pictos/tent.png', + size: { width: 120, height: 94 }, + position: { yaw: -0.45, pitch: -0.1 }, + tooltip: 'Image embedded in the scene', + }, + { + // html marker with custom style + id: 'text', + position: { yaw: 0, pitch: 0 }, + html: 'HTML marker ♥', + anchor: 'bottom right', + scale: [0.5, 1.5], + style: { + maxWidth: '100px', + color: 'white', + fontSize: '20px', + fontFamily: 'Helvetica, sans-serif', + textAlign: 'center', + }, + tooltip: { + content: 'An HTML marker', + position: 'right', + }, + }, + { + // polygon marker + id: 'polygon', + polyline: [ + [6.2208, 0.0906], [0.0443, 0.1028], [0.2322, 0.0849], [0.4531, 0.0387], + [0.5022, -0.0056], [0.4587, -0.0396], [0.252, -0.0453], [0.0434, -0.0575], + [6.1302, -0.0623], [6.0094, -0.0169], [6.0471, 0.032], [6.2208, 0.0906], + ], + svgStyle: { + fill: 'rgba(200, 0, 0, 0.2)', + stroke: 'rgba(200, 0, 50, 0.8)', + strokeWidth: '2px', + }, + tooltip: { + content: 'A dynamic polygon marker', + position: 'bottom right', + }, + }, + { + // polyline marker + id: 'polyline', + polylinePixels: [ + [2478, 1635], [2184, 1747], [1674, 1953], [1166, 1852], + [709, 1669], [301, 1519], [94, 1399], [34, 1356], + ], + svgStyle: { + stroke: 'rgba(140, 190, 10, 0.8)', + strokeLinecap: 'round', + strokeLinejoin: 'round', + strokeWidth: '10px', + }, + tooltip: 'A dynamic polyline marker', + }, + { + // circle marker + id: 'circle', + circle: 20, + position: { textureX: 2500, textureY: 1200 }, + tooltip: 'A circle marker', + }, + ], + }], + ], +}); + +const markersPlugin = viewer.getPlugin(PhotoSphereViewer.MarkersPlugin); + +/** + * Create a new marker when the user clicks somewhere + */ +viewer.addEventListener('click', ({ data }) => { + if (!data.rightclick) { + markersPlugin.addMarker({ + id: '#' + Math.random(), + position: { yaw: data.yaw, pitch: data.pitch }, + image: baseUrl + 'pictos/pin-red.png', + size: { width: 32, height: 32 }, + anchor: 'bottom center', + tooltip: 'Generated pin', + data: { + generated: true, + }, + }); + } +}); + +/** + * Delete a generated marker when the user double-clicks on it + * Or change the image if the user right-clicks on it + */ +markersPlugin.addEventListener('select-marker', ({ marker, doubleClick, rightClick }) => { + if (marker.data?.generated) { + if (doubleClick) { + markersPlugin.removeMarker(marker); + } else if (rightClick) { + markersPlugin.updateMarker({ + id: marker.id, + image: baseUrl + 'pictos/pin-blue.png', + }); + } + } +}); +``` + +```html + +``` + +::: + +::: tip +You can try markers live in [the playground](../playground.md). +::: + +## Markers definition + +One of these options is required. + +| Name | Type | Description | +| ---------------- | ---------------------------------------- | --------------------------------------------------------------------------------------- | +| `image` | `string` | Path to the image representing the marker. Requires `width` and `height` to be defined. | +| `imageLayer` | `string` | Path to the image representing the marker. Requires `width` and `height` to be defined. | +| `html` | `string` | HTML content of the marker. It is recommended to define `width` and `height`. | +| `square` | `integer` | Size of the square. | +| `rect` | `integer[2]`
`{width:int,height:int}` | Size of the rectangle. | +| `circle` | `integer` | Radius of the circle. | +| `ellipse` | `integer[2]`
`{cx:int,cy:int}` | Radiuses of the ellipse. | +| `path` | `string` | Definition of the path (0,0 will be placed at the defined `position`). | +| `polygon` | `double[2][]`
`string[2][]` | Array of points defining the polygon in spherical coordinates. | +| `polygonPixels` | `integer[2][]` | Same as above in pixel coordinates on the panorama image. | +| `polyline` | `double[2][]`
`string[2][]` | Same as `polygon` but generates a polyline. | +| `polylinePixels` | `integer[2][]` | Same as `polygonPixels` but generates a polyline. | + +**Examples :** + +```js +{ + image: 'pin-red.png', + imageLayer: 'pin-blue.png', + html: 'Click here', + square: 10, + rect: [10, 5], + rect: {width: 10, height: 5}, + circle: 10, + ellipse: [10, 5], + ellipse: {cx: 10, cy: 5}, + path: 'M 0 0 L 60 60 L 60 0 L 0 60 L 0 0', + polygon: [[0.2, 0.4], [0.9, 1.1], [1.5, 0.7]], + polygonPixels: [[100, 200], [150, 300], [300, 200]], + polyline: [[0.2, 0.4], [0.9, 1.1]], + polylinePixels: [[100, 200], [150, 300]], +} +``` + +::: tip What is the difference between "image" and "imageLayer" ? +Both allows to display an image but the difference is in the rendering technique. +And `image` marker is rendered flat above the viewer but and `imageLayer` is rendered inside the panorama itself, this allows for more natural movements and scaling. +::: + +## Markers options + +#### `id` (required) + +- type: `string` + +Unique identifier of the marker. + +#### `position` (required for all but polygons/polylines) + +- type: `{ yaw, pitch } | { textureX, textureY }` + +Position of the marker in **spherical coordinates** (radians/degrees) or **texture coordinates** (pixels). +_(This option is ignored for polygons and polylines)._ + +#### `size` (required for images, recommended for html) + +- type: `{ width, height }` + +Size of the marker in pixels. +_(This option is ignored for polygons and polylines)._ + +#### `orientation` (only for `imageLayer`) + +- type: `'front' | 'horizontal' | 'vertical-left' | 'vertical-right'` +- default: `'front'` + +Applies a perspective on the image to make it look like placed on the floor or on a wall. + +#### `scale` + +- type: `double[] | { zoom: double[], yaw: [] }` +- default: no scalling + +Configures the scale of the marker depending on the zoom level and/or the horizontal angle offset. This aims to give a natural feeling to the size of the marker as the users zooms and moves. +_(This option is ignored for polygons, polylines and imageLayer)._ + +:::: tabs + +::: tab Scale by zoom +Scales depending on zoom level, the array contains `[scale at minimum zoom, scale at maximum zoom]` : + +```js +scale: { + // the marker is twice smaller on the minimum zoom level + zoom: [0.5, 1]; +} +``` + +::: + +::: tab Scale by angle +Scales depending on position, the array contains `[scale on center, scale on the side]` : + +```js +scale: { + // the marker is twice bigger when on the side of the screen + yaw: [1, 2]; +} +``` + +::: + +::: tab Scale by zoom & angle +Of course the two configurations can be combined : + +```js +scale: { + zoom: [0.5, 1], + yaw: [1, 1.5] +} +``` + +::: + +:::: + +#### `opacity` + +- type: `number` +- default: `1` + +Opacity of the marker. + +#### `className` + +- type: `string` + +CSS class(es) added to the marker element. +_(This option is ignored for `imageLayer` markers)._ + +#### `style` + +- type: `object` + +CSS properties to set on the marker (background, border, etc.). +_(This option is ignored for `imageLayer` markers)._ + +```js +style: { + backgroundColor: 'rgba(0, 0, 0, 0.5)', + cursor : 'help' +} +``` + +#### `svgStyle` + +- type: `object` + +SVG properties to set on the marker (fill, stroke, etc.). +_(Only for polygons, polylines and svg markers)._ + +```js +svgStyle: { + fill : 'rgba(0, 0, 0, 0.5)', + stroke : '#ff0000', + strokeWidth: '2px' +} +``` + +::: tip Image and pattern background +You can define complex SVG backgrounds such as images by using a pattern definition. + +First declare the pattern somewhere in your page : + +```html + + + + + + + + +``` + +And use it in your marker : `fill: 'url(#image)'`. +::: + +#### `anchor` + +- type: `string` +- default: `'center center'` + +Defines where the marker is placed toward its defined position. Any CSS position is valid like `bottom center` or `20% 80%`. +_(This option is ignored for polygons and polylines)._ + +#### `zoomLvl` + +- type: `number` +- default: `undefind` + +The zoom level which will be applied when calling `gotoMarker()` method or when clicking on the marker in the list. +If not provided, the current zoom level is kept. + +#### `visible` + +- type: `boolean` +- default: `true` + +Initial visibility of the marker. + +#### `tooltip` + +- type: `string | {content: string, position: string, className: string, trigger: string}` +- default: `{content: null, position: 'top center', className: null, trigger: 'hover'}` + +Accepted positions are combinations of `top`, `center`, `bottom` and `left`, `center`, `right`. + +Possible triggers are `hover` and `click`. + +```js +tooltip: 'This is a marker' // tooltip with default position and style + +tooltip: { // tooltip with custom position + content: 'This is marker', + position: 'bottom left', +} + +tooltip: { // tooltip with a custom class shown on click + content: 'This is marker', + className: 'custom-tooltip', + trigger: 'click', +} +``` + +#### `content` + +- type: `string` + +HTML content that will be displayed on the side panel when the marker is clicked. + +#### `listContent` + +- type: `string` + +The name that appears in the list of markers. If not provided, the tooltip content will be used. + +#### `hideList` + +- type: `boolean` +- default: `false` + +Hide the marker in the markers list. + +#### `data` + +- type: `any` + +Any custom data you want to attach to the marker. You may access this data in the various [events](#events). + +## Configuration + +#### `lang` + +- type: `object` +- default: + +```js +lang: { + markers: 'Markers', + markersList: 'Markers list', +} +``` + +_Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ + +#### `markers` + +- type: `MarkerConfig[]` + +Initial list of markers. + +#### `gotoMarkerSpeed` + +- type: `string|number` +- default: `'8rpm'` + +Default animation speed for `gotoMarker` method. + +#### `clickEventOnMarker` + +- type: `boolean` +- default: `false` + +If a `click` event is triggered on the viewer additionally to the `select-marker` event. + +## Methods + +#### `addMarker(properties)` + +Adds a new marker to the viewer. + +```js +markersPlugin.addMarker({ + id: 'new-marker', + position: { yaw: '45deg', pitch: '0deg' }, + image: 'assets/pin-red.png', +}); +``` + +#### `clearMarkers()` + +Removes all markers. + +#### `getCurrentMarker(): Marker` + +Returns the last marker clicked by the user. + +#### `gotoMarker(id[, speed]): Animation` + +Moves the view to face a specific marker. + +```js +markersPlugin.gotoMarker('marker-1', '4rpm') + .then(() => /* animation complete */); +``` + +#### `hideMarker(id)` | `showMarker(id)` | `toggleMarker(id)` + +Changes the visiblity of a marker. + +#### `removeMarker(id)` | `removeMarkers(ids)` + +Removes a marker. + +#### `setMarkers(properties[])` + +Replaces all markers by new ones. + +#### `updateMarker(properties)` + +Updates a marker with new properties. The type of the marker cannot be changed. + +```js +markersPlugin.updateMarker({ + id: 'existing-marker', + image: 'assets/pin-blue.png', +}); +``` + +#### `showMarkerTooltip(id)` | `hideMarkerTooltip(id)` + +Allows to always display a tooltip. + +#### `showAllTooltips()` | `hideAllTooltips()` | `toggleAllTooltips()` + +Allows to always display all tooltips. + +## Events + +#### `select-marker(marker, doubleClick, rightClick)` + +Triggered when the user clicks on a marker. + +```js +markersPlugin.addEventListener('select-marker', ({ marker }) => { + console.log(`Clicked on marker ${marker.id}`); +}); +``` + +#### `unselect-marker(marker)` + +Triggered when a marker was selected and the user clicks elsewhere. + +#### `marker-visibility(marker, visible)` + +Triggered when the visibility of a marker changes. + +```js +markersPlugin.addEventListener('marker-visibility', ({ marker, visible }) => { + console.log(`Marker ${marker.id} is ${visible ? 'visible' : 'not visible'}`); +}); +``` + +#### `enter-marker(marker)` | `leave-marker(marker)` + +Triggered when the user puts the cursor hover or away a marker. + +## Buttons + +This plugin adds buttons to the default navbar: + +- `markers` allows to hide/show all markers +- `markersList` allows to open a list of all markers on the left panel + +If you use a [custom navbar](../guide/navbar.md) you will need to manually add the buttons to the list. diff --git a/docs/plugins/plugin-autorotate-keypoints.md b/docs/plugins/plugin-autorotate-keypoints.md deleted file mode 100644 index fa7b95429..000000000 --- a/docs/plugins/plugin-autorotate-keypoints.md +++ /dev/null @@ -1,153 +0,0 @@ -# AutorotateKeypointsPlugin - - - -> Replaces the standard autorotate animation by a smooth transition between multiple points. - -This plugin is available in the core `photo-sphere-viewer` package in `dist/plugins/autorotate-keypoints.js`. - -[[toc]] - - -## Usage - -The plugin is configured with `keypoints` which can be either a position object (either `x`/`y` or `longitude`/`latitude`) or the identifier of an existing [marker](./plugin-markers.md). - -It is also possible to configure each keypoint with a pause time and a tooltip. - -```js -const viewer = new PhotoSphereViewer.Viewer({ - plugins: [ - [PhotoSphereViewer.AutorotateKeypointsPlugin, { - keypoints: [ - 'existing-marker-id', - - { longitude: Math.PI / 2, latitude: 0 }, - - { - position: { longitude: Math.PI, latitude: Math.PI / 6 }, - pause : 5000, - tooltip : 'This is interesting', - }, - - { - markerId: 'another-marker', // will use the marker tooltip if any - pause : 2500, - }, - ], - }], - ], -}); -``` - -The plugin reacts to the standard `autorotateDelay` and `autorotateSpeed` options and can be started with `startAutorotate` or the button in the navbar. - - -## Example - -The following demo randomly generates some markers and automatically pan between them. - -::: code-demo - -```yaml -title: PSV Autorotate Keypoints Demo -resources: - - path: plugins/autorotate-keypoints.js - imports: AutorotateKeypointsPlugin - - path: plugins/markers.js - imports: MarkersPlugin - - path: plugins/markers.css -``` - -```js -const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; - -const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - autorotateDelay: 1000, - autorotateSpeed: '3rpm', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - - navbar: [ - 'autorotate', - 'zoom', - { - // a custom button to change keypoints - title: 'Change points', - content: '🔄', - onClick: randomPoints, - }, - 'caption', - 'fullscreen', - ], - - plugins: [ - PhotoSphereViewer.AutorotateKeypointsPlugin, - PhotoSphereViewer.MarkersPlugin, - ], -}); - -const autorotatePlugin = viewer.getPlugin(PhotoSphereViewer.AutorotateKeypointsPlugin); -const markersPlugin = viewer.getPlugin(PhotoSphereViewer.MarkersPlugin); - -viewer.once('ready', randomPoints); - -/** - * Randomize the keypoints and add corresponding markers - */ -function randomPoints() { - const points = []; - - for (let i = 0, l = Math.random() * 2 + 4; i < l; i++) { - points.push({ - position: { - longitude: (i + Math.random()) * 2 * Math.PI / l, - latitude: Math.random() * Math.PI / 3 - Math.PI / 6, - }, - pause: i % 3 === 0 ? 2000 : 0, - tooltip: 'Test tooltip', - }); - } - - markersPlugin.setMarkers(points.map((pt, i) => { - return { - id: '#' + i, - latitude: pt.position.latitude, - longitude: pt.position.longitude, - image: baseUrl + 'pictos/pin-red.png', - width: 32, - height: 32, - anchor: 'bottom center', - }; - })); - - autorotatePlugin.setKeypoints(points); -} -``` - -::: - - -## Configuration - -#### `startFromClosest` -- type: `boolean` -- default: `true` - -Start from the closest keypoint instead of the first keypoint of the array. - -#### `keypoints` -- type: `Keypoints[]` - -Initial keypoints, does the same thing as calling `setKeypoints` just after initialisation. - - -## Methods - -#### `setKeypoints(keypoints)` - -Changes or remove the keypoints. diff --git a/docs/plugins/plugin-compass.md b/docs/plugins/plugin-compass.md deleted file mode 100644 index cda1e5e60..000000000 --- a/docs/plugins/plugin-compass.md +++ /dev/null @@ -1,199 +0,0 @@ -# CompassPlugin - - - -> Adds a compass on the viewer to represent which portion of the sphere is currently visible. - -This plugin is available in the core `photo-sphere-viewer` package in `dist/plugins/compass.js` and `dist/plugins/compass.css`. - -[[toc]] - - -## Usage - -The plugin can be configured with `hotspots` which are small dots on the compass. It can also use markers. - -```js -const viewer = new PhotoSphereViewer.Viewer({ - plugins: [ - [PhotoSphereViewer.CompassPlugin, { - hotspots: [ - { longitude: '45deg' }, - { longitude: '60deg', color: 'red' }, - ], - }], - ], -}); -``` - - -## Example - -::: code-demo - -```yaml -title: PSV Compass Demo -resources: - - path: plugins/compass.js - imports: CompassPlugin - - path: plugins/compass.css - - path: plugins/markers.js - imports: MarkersPlugin - - path: plugins/markers.css -``` - -```js -const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; - -const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - - plugins: [ - [PhotoSphereViewer.CompassPlugin, { - hotspots: [ - { longitude: '0deg' }, - { longitude: '90deg' }, - { longitude: '180deg' }, - { longitude: '270deg' }, - ], - }], - [PhotoSphereViewer.MarkersPlugin, { - markers: [ - { - id: 'pin', - longitude: 0.11, - latitude: 0.32, - image: baseUrl + 'pictos/pin-blue.png', - width: 32, - height: 32, - anchor: 'bottom center', - data : { compass: '#304ACC' }, - }, - { - id: 'polygon', - polygonPx: [2941, 1413, 3042, 1402, 3222, 1419, 3433, 1463, 3480, 1505, 3438, 1538, 3241, 1543, 3041, 1555, 2854, 1559, 2739, 1516, 2775, 1469, 2941, 1413 ], - svgStyle : { - fill : 'rgba(255,0,0,0.2)', - stroke : 'rgba(255, 0, 50, 0.8)', - strokeWidth: '2px', - }, - data: { compass: 'rgba(255, 0, 50, 0.8)' }, - }, - { - id: 'polyline', - polylinePx: [2478, 1635, 2184, 1747, 1674, 1953, 1166, 1852, 709, 1669, 301, 1519, 94, 1399, 34, 1356], - svgStyle: { - stroke : 'rgba(80, 150, 50, 0.8)', - strokeLinecap : 'round', - strokeLinejoin: 'round', - strokeWidth : '20px', - }, - data: { compass: 'rgba(80, 150, 50, 0.8)' }, - }, - ], - }], - ], -}); -``` - -::: - -::: tip -The north is always at longitude=0, if you need to change where is the north you can use `panoData.poseHeading` or `sphereCorrection.pan` option. -::: - - -## Configuration - -#### `size` -- type: `string` -- default: `'120px'` - -The size of the compass, can be declared in `px`, `rem`, `vh`, etc. - -#### `position` -- type: `string` -- default: `'top left'` - -Accepted positions are combinations of `top`, `center`, `bottom` and `left`, `center`, `right`. - -#### `navigation` -- type: `boolean` -- default: `true` - -Allows to click on the compass to rotate the viewer. - -#### `hotspots` -- type: `Hotspot[]` -- default: `null` - -Small dots visible on the compass. Each spot consist of a position (either `x`/`y` or `longitude`/`latitude`) and an optional `color` which overrides the global `hotspotColor`. - -::: tip -[Markers](plugin-markers.md) can be displayed on the compass by defining their `compass` data which can be `true` or a specific color. - -```js -markers: [ - { - id: 'marker-1', - image: 'pin.png', - longitude: '15deg', latitude: 0, - data: { compass: true }, - }, - { - id: 'marker-2', - text: 'Warning', - longitude: '-45deg', latitude: 0, - data: { compass: 'orange' }, - }, -] -``` - -::: - -#### `backgroundSvg` -- type: `string` -- default: SVG provided by the plugin - -SVG used as background of the compass (must be square). - -#### `coneColor` -- type: `string` -- default: `'rgba(255, 255, 255, 0.2)'` - -Color of the cone of the compass. - -#### `navigationColor` -- type: `string` -- default: `'rgba(255, 0, 0, 0.2)'` - -Color of the navigation cone. - -#### `hotspotColor` -- type: `string` -- default: `'rgba(0, 0, 0, 0.5)'` - -Default color of hotspots. - - -## Methods - -#### `setHotspots(hotspots)` - -Changes the hotspots. - -```js -compassPlugin.setHotspots([ - { longitude: '0deg' }, - { longitude: '10deg', color: 'red' }, -]); -``` - -#### `clearHotspots()` - -Removes all hotspots diff --git a/docs/plugins/plugin-gallery.md b/docs/plugins/plugin-gallery.md deleted file mode 100644 index fb3fbead7..000000000 --- a/docs/plugins/plugin-gallery.md +++ /dev/null @@ -1,173 +0,0 @@ -# GalleryPlugin - - - -> Adds a gallery on the bottom of the viewer to navigate between multiple panoramas. - -This plugin is available in the core `photo-sphere-viewer` package in `dist/plugins/gallery.js` and `dist/plugins/gallery.css`. - -[[toc]] - -::: warning -GalleryPlugin is not compatible with ResolutionPlugin. -::: - - -## Usage - -The plugin has a list of `items`, each configuring the corresponding panorama, a name and a thumbnail. - -```js -const viewer = new PhotoSphereViewer.Viewer({ - plugins: [ - [PhotoSphereViewer.GalleryPlugin, { - items: [ - { - id: 'pano-1', - name: 'Panorama 1', - panorama: 'path/to/pano-1.jpg', - thumbnail: 'path/to/pano-1-thumb.jpg', - }, - { - id: 'pano-2', - name: 'Panorama 2', - panorama: 'path/to/pano-2.jpg', - thumbnail: 'path/to/pano-2-thumb.jpg', - }, - ], - }], - ], -}); -``` - - -## Example - -::: code-demo - -```yaml -title: PSV Gallery Demo -resources: - - path: plugins/gallery.js - imports: GalleryPlugin - - path: plugins/gallery.css -``` - -```js -const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; - -const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - - plugins: [ - [PhotoSphereViewer.GalleryPlugin, { - visibleOnLoad: true, - }], - ], -}); - -const gallery = viewer.getPlugin(PhotoSphereViewer.GalleryPlugin); - -gallery.setItems([ - { - id : 'sphere', - panorama : baseUrl + 'sphere.jpg', - thumbnail: baseUrl + 'sphere-small.jpg', - options : { - caption: 'Parc national du Mercantour © Damien Sorel', - }, - }, - { - id : 'sphere-test', - panorama: baseUrl + 'sphere-test.jpg', - name : 'Test sphere', - }, - { - id : 'key-biscayne', - panorama : baseUrl + 'tour/key-biscayne-1.jpg', - thumbnail: baseUrl + 'tour/key-biscayne-1-thumb.jpg', - name : 'Key Biscayne', - options : { - caption: 'Cape Florida Light, Key Biscayne © Pixexid', - }, - }, -]); -``` - -::: - - -## Configuration - -#### `items` -- type: `array` -- default: `[]` - -The list of items, see bellow. - -#### `visibleOnLoad` -- type: `boolean` -- default: `false` - -Displays the gallery when loading the first panorama. The user will be able to toggle the gallery with the navbar button. - -#### `hideOnClick` -- type: `boolean` -- default: `true - -Hides the gallery when the user clicks on an item. - -#### `thumbnailSize` -- type: `{ width: number, height: number }` -- default: `{ width: 200, height: 100 }` - -Size of the thumbnails. - -### Items - -#### `id` (required) -- type: `number|string` - -Unique identifier of the item. - -#### `thumbnail` (recommended) -- type: `string` -- default: `''` - -URL of the thumbnail. - -#### `name` -- type: `string` -- default: `''` - -Text visible over the thumbnail. - -#### `panorama` (required) - -Refer to the main [config page](../guide/config.md#panorama-required). - -#### `options` -- type: `PanoramaOptions` -- default: `null` - -Any option supported by the [setPanorama()](../guide/methods.md#setpanorama-panorama-options-promise) method. - - -## Methods - -#### `setItems(items)` - -Changes the list of items. - - -## Buttons - -This plugin adds buttons to the default navbar: -- `gallery` allows to toggle the gallery panel - -If you use a [custom navbar](../guide/navbar.md) you will need to manually add the buttons to the list. diff --git a/docs/plugins/plugin-markers.md b/docs/plugins/plugin-markers.md deleted file mode 100644 index 965462198..000000000 --- a/docs/plugins/plugin-markers.md +++ /dev/null @@ -1,611 +0,0 @@ -# MarkersPlugin - - - -> Displays various markers/hotspots on the viewer. - -This plugin is available in the core `photo-sphere-viewer` package in `dist/plugins/markers.js` and `dist/plugins/markers.css`. - -[[toc]] - -## Usage - -The plugin provides a powerful markers system allowing to define points of interest on the panorama with optional tooltip and description. Markers can be dynamically added/removed and you can react to user click/tap. - -There are four types of markers : - -- **HTML** defined with the `html` attribute -- **Images** defined with the `image` or `imageLayer` attribute -- **SVGs** defined with the `rect`, `circle`, `ellipse` or `path` attribute -- **Dynamic polygons & polylines** defined with the `polygonPx`/`polygonRad`/`polylinePx`/`polylineRad` attribute - -Markers can be added at startup with the `markers` option or after load with the various methods. - -```js -const viewer = new PhotoSphereViewer.Viewer({ - plugins: [ - [PhotoSphereViewer.MarkersPlugin, { - markers: [ - { - id: 'new-marker', - longitude: '45deg', - latitude: '0deg', - image: 'assets/pin-red.png', - }, - ], - }], - ], -}); - -const markersPlugin = viewer.getPlugin(PhotoSphereViewer.MarkersPlugin); - -markersPlugin.on('select-marker', (e, marker) => { - markersPlugin.updateMarker({ - id: marker.id, - image: 'assets/pin-blue.png' - }); -}); -``` - - -## Example - -The following example contains all types of markers. Click anywhere on the panorama to add a red marker, right-click to change it's color and double-click to remove it. - -::: code-demo - -```yaml -title: PSV Markers Demo -resources: - - path: plugins/markers.js - imports: MarkersPlugin - - path: plugins/markers.css -``` - -```js -const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; - -const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - - plugins: [ - [PhotoSphereViewer.MarkersPlugin, { - // list of markers - markers: [{ - // image marker that opens the panel when clicked - id: 'image', - longitude: 0.32, - latitude: 0.11, - image: baseUrl + 'pictos/pin-blue.png', - width: 32, - height: 32, - anchor: 'bottom center', - zoomLvl: 100, - tooltip: 'A image marker. Click me!', - content: document.getElementById('lorem-content').innerHTML - }, - { - // image marker rendered in the 3D scene - id : 'imageLayer', - imageLayer: baseUrl + 'pictos/tent.png', - width : 120, - height : 94, - longitude : -0.45, - latitude : -0.1, - tooltip : 'Image embedded in the scene', - }, - { - // html marker with custom style - id: 'text', - longitude: 0, - latitude: 0, - html: 'HTML marker ♥', - anchor: 'bottom right', - scale: [0.5, 1.5], - style: { - maxWidth: '100px', - color: 'white', - fontSize: '20px', - fontFamily: 'Helvetica, sans-serif', - textAlign: 'center' - }, - tooltip: { - content: 'An HTML marker', - position: 'right' - } - }, - { - // polygon marker - id: 'polygon', - polylineRad: [ - [6.2208, 0.0906], [0.0443, 0.1028], [0.2322, 0.0849], [0.4531, 0.0387], - [0.5022, -0.0056], [0.4587, -0.0396], [0.2520, -0.0453], [0.0434, -0.0575], - [6.1302, -0.0623], [6.0094, -0.0169], [6.0471, 0.0320], [6.2208, 0.0906], - ], - svgStyle: { - fill: 'rgba(200, 0, 0, 0.2)', - stroke: 'rgba(200, 0, 50, 0.8)', - strokeWidth: '2px' - }, - tooltip: { - content: 'A dynamic polygon marker', - position: 'bottom right' - } - }, - { - // polyline marker - id: 'polyline', - polylinePx: [ - [2478, 1635], [2184, 1747], [1674, 1953], [1166, 1852], - [709, 1669], [301, 1519], [94, 1399], [34, 1356] - ], - svgStyle: { - stroke: 'rgba(140, 190, 10, 0.8)', - strokeLinecap: 'round', - strokeLinejoin: 'round', - strokeWidth: '10px' - }, - tooltip: 'A dynamic polyline marker' - }, - { - // circle marker - id: 'circle', - circle: 20, - x: 2500, - y: 1200, - tooltip: 'A circle marker' - } - ] - }] - ] -}); - -const markersPlugin = viewer.getPlugin(PhotoSphereViewer.MarkersPlugin); - -/** - * Create a new marker when the user clicks somewhere - */ -viewer.on('click', (e, data) => { - if (!data.rightclick) { - markersPlugin.addMarker({ - id: '#' + Math.random(), - longitude: data.longitude, - latitude: data.latitude, - image: baseUrl + 'pictos/pin-red.png', - width: 32, - height: 32, - anchor: 'bottom center', - tooltip: 'Generated pin', - data: { - generated: true - } - }); - } -}); - -/** - * Delete a generated marker when the user double-clicks on it - * Or change the image if the user right-clicks on it - */ -markersPlugin.on('select-marker', (e, marker, data) => { - if (marker.data && marker.data.generated) { - if (data.dblclick) { - markersPlugin.removeMarker(marker); - } else if (data.rightclick) { - markersPlugin.updateMarker({ - id: marker.id, - image: baseUrl + 'pictos/pin-blue.png', - }); - } - } -}); -``` - -```html - -``` - -::: - -::: tip -You can try markers live in [the playground](../playground.md). -::: - - -## Markers definition - -One of these options is required. - -| Name | Type | Description | -|---|---|---| -| `image` | `string` | Path to the image representing the marker. Requires `width` and `height` to be defined. | -| `imageLayer` | `string` | Path to the image representing the marker. Requires `width` and `height` to be defined. | -| `html` | `string` | HTML content of the marker. It is recommended to define `width` and `height`. | -| `square` | `integer` | Size of the square. | -| `rect` | `integer[2] |`
`{width:int,height:int}` | Size of the rectangle. | -| `circle` | `integer` | Radius of the circle. | -| `ellipse` | `integer[2] |`
`{cx:int,cy:int}` | Radiuses of the ellipse. | -| `path` | `string` | Definition of the path (0,0 will be placed at the defined x/y or longitude/latitude). | -| `polygonPx` | `integer[2][]` |Array of points defining the polygon in pixel coordinates on the panorama image. | -| `polygonRad` | `double[2][]` | Same as above but coordinates are in longitude and latitude. | -| `polylinePx` | `integer[2][]` | Same as `polygonPx` but generates a polyline. | -| `polylineRad` | `double[2][]` | Same as `polygonRad` but generates a polyline. | - -**Examples :** - -```js -{ - image: 'pin-red.png', - imageLayer: 'pin-blue.png', - html: 'Click here', - square: 10, - rect: [10, 5], - rect: {width: 10, height: 5}, - circle: 10, - ellipse: [10, 5], - ellipse: {cx: 10, cy: 5}, - path: 'M 0 0 L 60 60 L 60 0 L 0 60 L 0 0', - polygonPx: [[100, 200], [150, 300], [300, 200]], - polygonRad: [[0.2, 0.4], [0.9, 1.1], [1.5, 0.7]], - polylinePx: [[100, 200], [150, 300]], - polylineRad: [[0.2, 0.4], [0.9, 1.1]], -} -``` - -::: tip What is the difference between "image" and "imageLayer" ? -Both allows to display an image but the difference is in the rendering technique. -And `image` marker is rendered flat above the viewer but and `imageLayer` is rendered inside the panorama itself, this allows for more natural movements and scaling. -::: - -::: warning -Texture coordinates are not applicable to cubemaps. -::: - - - -## Markers options - -#### `id` (required) -- type: `string` - -Unique identifier of the marker. - -#### `x` & `y` or `latitude` & `longitude` (required for all but polygons/polylines) -- type: `integer` or `double` - -Position of the marker in **texture coordinates** (pixels) or **spherical coordinates** (radians). -_(This option is ignored for polygons and polylines)._ - -#### `width` & `height` (required for images, recommended for html) -- type: `integer` - -Size of the marker in pixels. -_(This option is ignored for polygons and polylines)._ - -#### `orientation` (only for `imageLayer`) -- type: `'front' | 'horizontal' | 'vertical-left' | 'vertical-right'` -- default: `'front'` - -Applies a perspective on the image to make it look like placed on the floor or on a wall. - -#### `scale` -- type: `double[] | { zoom: double[], longitude: [] }` -- default: no scalling - -Configures the scale of the marker depending on the zoom level and/or the longitude offset. This aims to give a natural feeling to the size of the marker as the users zooms and moves. -_(This option is ignored for polygons, polylines and imageLayer)._ - -Scales depending on zoom level, the array contains `[scale at minimum zoom, scale at maximum zoom]` : -```js -scale: { - // the marker is twice smaller on the minimum zoom level - zoom: [0.5, 1] -} -``` - -Scales depending on position, the array contains `[scale on center, scale on the side]` : -```js -scale: { - // the marker is twice bigger when on the side of the screen - longitude: [1, 2] -} -``` - -Of course the two configurations can be combined : -```js -scale: { - zoom: [0.5, 1], - longitude: [1, 1.5] -} -``` - -#### `opacity` -- type: `number` -- default: `1` - -Opacity of the marker. (Works for `imageLayer` too). - -#### `className` -- type: `string` - -CSS class(es) added to the marker element. -_(This option is ignored for `imageLayer` markers)._ - -#### `style` -- type: `object` - -CSS properties to set on the marker (background, border, etc.). -_(This option is ignored for `imageLayer` markers)._ - -```js -style: { - backgroundColor: 'rgba(0, 0, 0, 0.5)', - cursor : 'help' -} -``` - -#### `svgStyle` -- type: `object` - -SVG properties to set on the marker (fill, stroke, etc.). -_(Only for polygons, polylines and svg markers)._ - -```js -svgStyle: { - fill : 'rgba(0, 0, 0, 0.5)', - stroke : '#ff0000', - strokeWidth: '2px' -} -``` - -::: tip Image and pattern background -You can define complex SVG backgrounds such as images by using a pattern definition. - -First declare the pattern somewhere in your page : - -```html - - - - - - - - -``` - -And use it in your marker : `fill: 'url(#image)'`. -::: - -#### `anchor` -- type: `string` -- default: `'center center'` - -Defines where the marker is placed toward its defined position. Any CSS position is valid like `bottom center` or `20% 80%`. -_(This option is ignored for polygons and polylines)._ - -#### `zoomLvl` -- type: `number` -- default: `undefind` - -The zoom level which will be applied when calling `gotoMarker()` method or when clicking on the marker in the list. -If not provided, the current zoom level is kept. - -#### `visible` -- type: `boolean` -- default: `true` - -Initial visibility of the marker. - -#### `tooltip` -- type: `string | {content: string, position: string, className: string, trigger: string}` -- default: `{content: null, position: 'top center', className: null, trigger: 'hover'}` - -Accepted positions are combinations of `top`, `center`, `bottom` and `left`, `center`, `right`. - -Possible triggers are `hover` and `click`. - -```js -tooltip: 'This is a marker' // tooltip with default position and style - -tooltip: { // tooltip with custom position - content: 'This is marker', - position: 'bottom left', -} - -tooltip: { // tooltip with a custom class shown on click - content: 'This is marker', - className: 'custom-tooltip', - trigger: 'click', -} -``` - -::: warning -If `trigger` is set to `'click'` you won't be able to display a `content` in the side panel. -::: - -#### `content` -- type: `string` - -HTML content that will be displayed on the side panel when the marker is clicked. - -#### `listContent` -- type: `string` - -The name that appears in the list of markers. If not provided, the tooltip content will be used. - -#### `hideList` -- type: `boolean` -- default: `false` - -Hide the marker in the markers list. - -#### `data` -- type: `any` - -Any custom data you want to attach to the marker. You may access this data in the various [events](#events). - - -## Configuration - -#### `lang` -- type: `object` -- default: -```js -lang: { - markers : 'Markers', - markersList: 'Markers list', -} -``` - -_Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ - -#### `gotoMarkerSpeed` -- type: `string|number` -- default `'8rpm'` - -Default animation speed for `gotoMarker` method. - -#### `clickEventOnMarker` -- type: `boolean` -- default: `false` - -If a `click` event is triggered on the viewer additionally to the `select-marker` event. - - -## Methods - -#### `addMarker(properties)` - -Adds a new marker to the viewer. - -```js -markersPlugin.addMarker({ - id: 'new-marker', - longitude: '45deg', - latitude: '0deg', - image: 'assets/pin-red.png', -}); -``` - -#### `clearMarkers()` - -Removes all markers. - -#### `getCurrentMarker(): Marker` - -Returns the last marker clicked by the user. - -#### `gotoMarker(id[, speed]): Animation` - -Moves the view to face a specific marker. - -```js -markersPlugin.gotoMarker('marker-1', '4rpm') - .then(() => /* animation complete */); -``` - -#### `hideMarker(id)` | `showMarker(id)` | `toggleMarker(id)` - -Changes the visiblity of a marker. - -#### `removeMarker(id)` | `removeMarkers(ids)` - -Removes a marker. - -#### `setMarkers(properties[])` - -Replaces all markers by new ones. - -#### `updateMarker(properties)` - -Updates a marker with new properties. The type of the marker cannot be changed. - -```js -markersPlugin.updateMarker({ - id: 'existing-marker', - image: 'assets/pin-blue.png', -}); -``` - -#### `showMarkerTooltip(id)` | `hideMarkerTooltip(id)` - -Allows to always display a tooltip. - -#### `showAllTooltips()` | `hideAllTooltips()` | `toggleAllTooltips()` - -Allows to always display all tooltips. - - -## Events - -#### `marker-visibility(marker, visible)` - -Triggered when the visibility of a marker changes. - -```js -markersPlugin.on('marker-visibility', (e, marker, visible) => { - console.log(`Marker ${marker.id} is ${visible ? 'visible' : 'not visible'}`); -}); -``` - -#### `over-marker(marker)` | `leave-marker(marker)` - -Triggered when the user puts the cursor hover or away a marker. - -```js -markersPlugin.on('over-marker', (e, marker) => { - console.log(`Cursor is over marker ${marker.id}`); -}); -``` - -#### `select-marker(marker, data)` - -Triggered when the user clicks on a marker. The `data` object indicates if the marker was selected with a double a click on a right click. - -#### `unselect-marker(marker)` - -Triggered when a marker was selected and the user clicks elsewhere. - - -## Buttons - -This plugin adds buttons to the default navbar: -- `markers` allows to hide/show all markers -- `markersList` allows to open a list of all markers on the left panel - -If you use a [custom navbar](../guide/navbar.md) you will need to manually add the buttons to the list. diff --git a/docs/plugins/plugin-resolution.md b/docs/plugins/plugin-resolution.md deleted file mode 100644 index 4b20084a3..000000000 --- a/docs/plugins/plugin-resolution.md +++ /dev/null @@ -1,138 +0,0 @@ -# ResolutionPlugin - - - -> Adds a button to choose between multiple resolutions of the panorama. **Requires the [Settings plugin](./plugin-settings.md).** - -This plugin is available in the core `photo-sphere-viewer` package in `dist/plugins/resolution.js`. - -[[toc]] - -::: warning -ResolutionPlugin is not compatible with GalleryPlugin. -::: - - -## Usage - -Once enabled the plugin will add a new setting the user can use to change the resolution of the panorama. - -```js -const viewer = new PhotoSphereViewer.Viewer({ - plugins: [ - PhotoSphereViewer.SettingsPlugin, - [PhotoSphereViewer.ResolutionPlugin, { - defaultResolution: 'SD', - resolutions: [ - { - id : 'SD', - label : 'Small', - panorama: 'sphere_small.jpg', - }, - { - id : 'HD', - label : 'Normal', - panorama: 'sphere.jpg', - }, - ], - }], - ], -}); -``` - - -## Example - -The following example provides two resolutions for the panorama, "small" is loaded by default. - -::: code-demo - -```yaml -title: PSV Resolution Demo -resources: - - path: plugins/settings.js - imports: SettingsPlugin - - path: plugins/settings.css - - path: plugins/resolution.js - imports: ResolutionPlugin -``` - -```js -const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; - -const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - - plugins: [ - PhotoSphereViewer.SettingsPlugin, - [PhotoSphereViewer.ResolutionPlugin, { - defaultResolution: 'SD', - resolutions: [ - { - id : 'SD', - label : 'Small', - panorama: baseUrl + 'sphere-small.jpg', - }, - { - id : 'HD', - label : 'Normal', - panorama: baseUrl + 'sphere.jpg', - }, - ], - }], - ], -}); -``` - -::: - -## Configuration - -#### `resolutions` -- type: `object[]` - -List of available resolutions. Each resolution consist of an object with the properties `id`, `label` and `panorama`. -Cubemaps are supported. - -#### `defaultResolution` -- type: `string` - -The id of the default resolution to load. If not provided the first resolution will be used. - -::: warning -If a `panorama` is initially configured on the viewer, this setting is ignored. -::: - -#### `showBadge` -- type: `boolean` -- default: `true` - -Show the resolution id as a badge on the settings button. - -#### `lang` -- type: `object` -- default: -```js -lang: { - resolution : 'Quality', -} -``` - -_Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ - - -## Events - -#### `resolution-changed(id)` - -Triggered when the resolution is changed. - -```js -resolutionPlugin.on('resolution-changed', (e, id) => { - console.log(`Current resolution: ${id}`); -}); -``` diff --git a/docs/plugins/plugin-stereo.md b/docs/plugins/plugin-stereo.md deleted file mode 100644 index 5735f3430..000000000 --- a/docs/plugins/plugin-stereo.md +++ /dev/null @@ -1,47 +0,0 @@ -# StereoPlugin - - - -> Adds stereo view on mobile devices. **Requires the [Gyroscope plugin](./plugin-gyroscope.md).** - -This plugin is available in the core `photo-sphere-viewer` package in `dist/plugins/stereo.js`. - - -## Usage - -Once enabled the plugin will add a new "Stereo view" button only shown when the gyroscope API is available. - -The plugin uses the WakeLock API to prevent the display from dimming or shuting down. As of August 2020 this API is only available on Chrome and Edge, for others browsers you can install [NoSleep.js](http://richtr.github.io/NoSleep.js) (no further configuration is needed, just make it available in your page). - -```js -const viewer = new PhotoSphereViewer.Viewer({ - plugins: [ - PhotoSphereViewer.GyroscopePlugin, - PhotoSphereViewer.StereoPlugin, - ], -}); -``` - - -## Configuration - -#### `lang` -- type: `object` -- default: -```js -lang: { - stereo : 'Stereo view', - stereoNotification : 'Click anywhere to exit stereo view.', - pleaseRotate : ['Please rotate your device', '(or tap to continue)'], -} -``` - -_Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ - - -## Buttons - -This plugin adds buttons to the default navbar: -- `stereo` allows to start the stereo view - -If you use a [custom navbar](../guide/navbar.md) you will need to manually add the buttons to the list. diff --git a/docs/plugins/plugin-video.md b/docs/plugins/plugin-video.md deleted file mode 100644 index a203bada7..000000000 --- a/docs/plugins/plugin-video.md +++ /dev/null @@ -1,215 +0,0 @@ -# VideoPlugin - - - -> Adds controls to the video [adapters](../guide/adapters). - -This plugin is available in the core `photo-sphere-viewer` package in `dist/plugins/video.js` and `dist/plugins/video.css`. - - -## Usage - -To use this plugin you must also load one of the video adapters : [equirectangular](../guide/adapters/equirectangular-video.md) or [cubemap](../guide/adapters/cubemap-video.md). - -Once enabled it will add various elements to the viewer: - -- Play/pause button -- Volume button -- time indicator in the navbar -- Progressbar above the navbar -- Play button in the center of the viewer - -It also supports advanced autorotate with timed `keypoints`. - -```js -const viewer = new PhotoSphereViewer.Viewer({ - adapter: PhotoSphereViewer.EquirectangularVideoAdapter, - panorama: { - source: 'path/video.mp4', - }, - plugins: [ - [PhotoSphereViewer.VideoPlugin, {}], - ], -}); -``` - -### Multi resolution - -You can offer multiple resolutions of your video with the [ResolutionPlugin](./plugin-resolution.md). - -```js -const viewer = new PhotoSphereViewer.Viewer({ - adapter: PhotoSphereViewer.EquirectangularVideoAdapter, - plugins: [ - PhotoSphereViewer.VideoPlugin, - PhotoSphereViewer.SettingsPlugin, - [PhotoSphereViewer.ResolutionPlugin, { - defaultResolution: 'FHD', - resolutions: [ - { - id : 'UHD', - label : 'Ultra high', - panorama: { source: 'path/video-uhd.mp4' }, - }, - { - id : 'FHD', - label : 'High', - panorama: { source: 'path/video-fhd.mp4' }, - }, - { - id : 'HD', - label : 'Standard', - panorama: { source: 'path/video-hd.mp4' }, - }, - ], - }], - ], -}); -``` - -## Example - -::: code-demo - -```yaml -title: PSV Video Demo -resources: - - path: adapters/equirectangular-video.js - imports: EquirectangularVideoAdapter - - path: plugins/video.js - imports: VideoPlugin - - path: plugins/video.css - - path: plugins/settings.js - imports: SettingsPlugin - - path: plugins/settings.css - - path: plugins/resolution.js - imports: ResolutionPlugin -``` - -```js -const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; - -const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - adapter: [PhotoSphereViewer.EquirectangularVideoAdapter, { - muted: true, - }], - caption: 'Ayutthaya © meetle', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - navbar: 'video autorotate caption settings fullscreen', - - plugins: [ - [PhotoSphereViewer.VideoPlugin, { - keypoints : [ - { time: 0, position: { longitude: 0, latitude: 0 } }, - { time: 5, position: { longitude: -Math.PI / 4, latitude: Math.PI / 8 } }, - { time: 10, position: { longitude: -Math.PI / 2, latitude: 0 } }, - { time: 15, position: { longitude: -3 * Math.PI / 4, latitude: -Math.PI / 8 } }, - { time: 20, position: { longitude: -Math.PI, latitude: 0 } }, - { time: 25, position: { longitude: -5 * Math.PI / 4, latitude: Math.PI / 8 } }, - { time: 30, position: { longitude: -3 * Math.PI / 2, latitude: 0 } }, - { time: 35, position: { longitude: -7 * Math.PI / 4, latitude: -Math.PI / 8 } }, - ] - }], - PhotoSphereViewer.SettingsPlugin, - [PhotoSphereViewer.ResolutionPlugin, { - defaultResolution: 'HD', - resolutions: [ - { - id : 'UHD', - label : 'Ultra high', - panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_UHD.mp4' }, - }, - { - id : 'FHD', - label : 'High', - panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_FHD.mp4' }, - }, - { - id : 'HD', - label : 'Standard', - panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_HD.mp4' }, - }, - { - id : 'SD', - label : 'Low', - panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_SD.mp4' }, - }, - ] - }] - ], -}); -``` - -::: - - -## Configuration - -#### `keypoints` -- type: `Array<{ position, time }>` - -Defines timed keypoints that will be used by the autorotate button. - -```js -keypoints: [ - { time: 0, position: { longitude: 0, latitude: 0 } }, - { time: 5.5, position: { longitude: 0.25, latitude: 0 } }, - { time: 12.8, position: { longitude: 0.3, latitude: -12 } }, -] -``` - -#### `progressbar` -- type: `boolean` -- default: `true` - -Displays a progressbar on top of the navbar. - -#### `bigbutton` -- type: `boolean` -- default: `true` - -Displays a big "play" button in the center of the viewer. - -#### `lang` -- type: `object` -- default: -```js -lang: { - videoPlay : 'Play/Pause', - videoVolume: 'Volume', -} -``` - -_Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ - - -## Events - -#### `play` - -Triggered when the video starts playing. - -#### `pause` - -Triggered when the video is paused. - -#### `volume-change(volume)` - -Triggered when the video volume changes. - -#### `progress({ time, duration, progress })` - -Triggered when the video play progression changes. - - -## Buttons - -This plugin adds buttons to the default navbar: -- `videoPlay` allows to play/pause the video -- `videoVolume` allows to change the volume/mute the video -- `videoTime` shows the video time and duration (not a button) - -If you use a [custom navbar](../guide/navbar.md) you will need to manually add the buttons to the list. diff --git a/docs/plugins/plugin-virtual-tour.md b/docs/plugins/plugin-virtual-tour.md deleted file mode 100644 index 9f3f8ac5b..000000000 --- a/docs/plugins/plugin-virtual-tour.md +++ /dev/null @@ -1,467 +0,0 @@ -# VirtualTourPlugin - - - -> Create virtual tours by linking multiple panoramas. - -This plugin is available in the core `photo-sphere-viewer` package in `dist/plugins/virtual-tour.js` and `dist/plugins/virtual-tour.css`. - -[[toc]] - -## Usage - -The plugin allows to define `nodes` which contains a `panorama` and one or more `links` to other nodes. The links are represented with a 3D arrow (default) or using the [Markers plugin](./plugin-markers.md). - -There are two different ways to define the position of the links : the manual mode and the GPS mode. - -:::: tabs - -::: tab Manual mode -In manual mode each link must have `longitude`/`latitude` or `x`/`y` coordinates to be placed at the correct location on the panorama. This works exactly like the placement of markers. - -```js -const node = { - id: 'node-1', - panorama: '001.jpg', - links: [{ - nodeId: 'node-2', - x: 1500, - y: 780, - }], -}; -``` -::: - -::: tab GPS mode -In GPS mode each node has positionning coordinates and the links are placed automatically. - -```js -const node = { - id: 'node-1', - panorama: '001.jpg', - position: [-80.156479, 25.666725], // optional altitude as 3rd value - links: [{ - nodeId: 'node-2', - position: [-80.156168, 25.666623], // the position of the linked node must be provided here in server mode - }], -}; -``` -::: - -:::: - - -The nodes can be provided all at once or asynchronously as the user navigates. - -:::: tabs - -::: tab Client mode -In client mode you must provide all `nodes` all at once, you can also change all the nodes with the `setNodes` method. - -```js -const nodes = [ - { id: 'node-1', panorama: '001.jpg', links: [{ nodeId: 'node-2', x: 1500, y: 780}] }, - { id: 'node-2', panorama: '002.jpg', links: [{ nodeId: 'node-1', x: 3000, y: 780}] }, -]; -``` -::: - -::: tab Server mode -In server mode you provide the `getNode callbacks function which returns a Promise to load the data of a node. - -```js -getNode = async (nodeId) => { - const res = await fetch(`/api/nodes/${nodeId}`); - return await res.json(); -}; -``` -::: - -:::: - -::: tip -If the [Gallery plugin](./plugin-gallery.md) is loaded, it will be configured with the list of nodes (client mode only). -::: - - -## Example - -::: code-demo - -```yaml -title: PSV Virtual Tour Demo -resources: - - path: plugins/virtual-tour.js - imports: VirtualTourPlugin - - path: plugins/virtual-tour.css - - path: plugins/gallery.js - imports: GalleryPlugin - - path: plugins/gallery.css - - path: plugins/markers.js - imports: MarkersPlugin - - path: plugins/markers.css -``` - -```js -const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; - -const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - caption: 'Cape Florida Light, Key Biscayne © Pixexid', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - defaultLong: '130deg', - navbar: 'zoom move download gallery caption fullscreen', - - plugins: [ - PhotoSphereViewer.MarkersPlugin, - [PhotoSphereViewer.GalleryPlugin, { - thumbnailSize: { width: 100, height: 100 }, - }], - [PhotoSphereViewer.VirtualTourPlugin, { - positionMode: PhotoSphereViewer.VirtualTourPlugin.MODE_GPS, - renderMode : PhotoSphereViewer.VirtualTourPlugin.MODE_3D, - }], - ], -}); - -const virtualTour = viewer.getPlugin(PhotoSphereViewer.VirtualTourPlugin); - -virtualTour.setNodes([ - { - id : '1', - panorama: baseUrl + 'tour/key-biscayne-1.jpg', - thumbnail: baseUrl + 'tour/key-biscayne-1-thumb.jpg', - name : 'One', - links : [ - { nodeId: '2' }, - ], - markers: [ - { - id: 'marker-1', - image: baseUrl + 'pictos/pin-red.png', - tooltip: 'Cape Florida Light, Key Biscayne', - width : 32, - height : 32, - anchor : 'bottom center', - longitude: '105deg', - latitude: '35deg', - } - ], - position: [-80.156479, 25.666725, 3], - panoData: { poseHeading: 327 }, - }, - { - id : '2', - panorama: baseUrl + 'tour/key-biscayne-2.jpg', - thumbnail: baseUrl + 'tour/key-biscayne-2-thumb.jpg', - name : 'Two', - links : [ - { nodeId: '3' }, - { nodeId: '1' }, - ], - position: [-80.156168, 25.666623, 3], - panoData: { poseHeading: 318 }, - }, - { - id : '3', - panorama: baseUrl + 'tour/key-biscayne-3.jpg', - thumbnail: baseUrl + 'tour/key-biscayne-3-thumb.jpg', - name : 'Three', - links : [ - { nodeId: '4' }, - { nodeId: '2' }, - { nodeId: '5' }, - ], - position: [-80.155932, 25.666498, 5], - panoData: { poseHeading: 328 }, - }, - { - id : '4', - panorama: baseUrl + 'tour/key-biscayne-4.jpg', - thumbnail: baseUrl + 'tour/key-biscayne-4-thumb.jpg', - name : 'Four', - links : [ - { nodeId: '3' }, - { nodeId: '5' }, - ], - position: [-80.156089, 25.666357, 3], - panoData: { poseHeading: 78 }, - }, - { - id : '5', - panorama: baseUrl + 'tour/key-biscayne-5.jpg', - thumbnail: baseUrl + 'tour/key-biscayne-5-thumb.jpg', - name : 'Five', - links : [ - { nodeId: '6' }, - { nodeId: '3' }, - { nodeId: '4' }, - ], - position: [-80.156292, 25.666446, 2], - panoData: { poseHeading: 190 }, - }, - { - id : '6', - panorama: baseUrl + 'tour/key-biscayne-6.jpg', - thumbnail: baseUrl + 'tour/key-biscayne-6-thumb.jpg', - name : 'Six', - links : [ - { nodeId: '5' }, - { nodeId: '7' }, - ], - position: [-80.156465, 25.666496, 2], - panoData: { poseHeading: 328 }, - }, - { - id : '7', - panorama : baseUrl + 'tour/key-biscayne-7.jpg', - thumbnail: baseUrl + 'tour/key-biscayne-7-thumb.jpg', - name : 'Seven', - links : [ - { nodeId: '6' }, - ], - position : [-80.157070, 25.666500, 3], - panoData : { poseHeading: 250 }, - }, -], '2'); -``` - -::: - - -## Nodes options - -#### `id` (required) -- type: `string` - -Unique identifier of the node - -#### `panorama` (required) - -Refer to the main [config page](../guide/config.md#panorama-required). - -#### `links` (required in client mode) -- type: `array` - -Definition of the links of this node. See bellow. - -#### `position` (required in GPS mode) -- type: `number[]` - -GPS coordinates of this node as an array of two or three values (`[longitude, latitude, altitude]`). - -::: warning Projection system -Only the [ESPG:4326 projection](https://epsg.io/4326) is supported. -::: - -#### `name` -- type: `string` - -Short name of this node, used in links tooltips and the gallery. - -#### `caption` - -Replace the global caption. Refer to the main [config page](../guide/config.md#caption). - -#### `description` - -Replace the global description. Refer to the main [config page](../guide/config.md#description). - -#### `thumbnail` -- type: `string` - -Thumbnail for the nodes list in the gallery. - -#### `markers` -- type: `array` - -Additional markers displayed on this node, requires the [Markers plugin](./plugin-markers.md). - -#### `panoData` - -Refer to the main [config page](../guide/config.md#panodata). - -#### `sphereCorrection` - -Refer to the main [config page](../guide/config.md#spherecorrection). - - -## Links options - -#### `nodeId` (required) -- type: `string` - -Identifier of the target node. - -#### `x` & `y` or `latitude` & `longitude` (required in manual mode) -- type: `integer` or `double` - -Position of the link in **texture coordinates** (pixels) or **spherical coordinates** (radians). - -#### `position` (required in GPS+server mode) -- type: `number[]` - -Overrides the GPS coordinates of the target node. - -#### `name` -- type: `string` - -Overrides the tooltip content (defaults to the node's `name` property). - -#### `arrowStyle` (3d mode only) -- type: `object` - -Overrides the global style of the arrow used to display the link. See global configuration for details. - -#### `markerStyle` (markers mode only) -- type: `object` - -Overrides the global style of the marker used to display the link. See global configuration for details. - - -## Configuration - -#### `dataMode` -- type: `'client' | 'server'` -- default: `'client'` - -Configure how the nodes configuration is provided. - -#### `positionMode` -- type: `'manual' | 'gps'` -- default: `'manual'` - -Configure how the links between nodes are positionned. - -#### `renderMode` -- type: `'markers' | '3d'` -- default: `'3d'` - -How the links are displayed, `markers` requires the [Markers plugin](./plugin-markers.md). - -#### `nodes` (client mode only) -- type: `array` - -Initial list of nodes. You can also call `setNodes` method later. - -#### `getNode(nodeId)` (required in server mode) -- type: `function(nodeId: string) => Promise` - -Callback to load the configuration of a node. - -#### `startNodeId` -- type: `string` - -Id of the initially loaded node. If empty the first node will be displayed. You can also call `setCurrentNode` method later. - -#### `preload` -- type: `boolean | function(node: Node, link: NodeLink) => boolean` -- default: `false` - -Enable the preloading of linked nodes, can be a function that returns true or false for each link. - -#### `rotateSpeed` -- type: `boolean | string | number` -- default: `20rpm` - -When a link is clicked, adds a panorama rotation to face it before actually changing the node. If `false` the viewer won't rotate at all and keep the current orientation. - -#### `transition` -- type: `boolean | number` -- default: `1500` - -Duration of the transition between nodes. - -#### `linksOnCompass` -- type: `boolean` -- default: `true` if markers render mode - -If the [Compass plugin](plugin-compass.md) is enabled, displays the links on the compass. - -#### `markerStyle` (markers mode only) -- type: `object` - -Style of the marker used to display links. - -Default value is: -```js -{ - html : arrowIconSvg, // an SVG provided by the plugin - width : 80, - height : 80, - scale : [0.5, 2], - anchor : 'top center', - className: 'psv-virtual-tour__marker', - style : { - color: 'rgba(0, 208, 255, 0.8)', - }, -} -``` - -::: tip -If you want to use another marker type like `image` you must define `html: null` to remove the default value. -```js -markerStyle: { - html : null, - image: 'path/to/image.png', -} -``` -::: - -#### `arrowStyle` (3d mode only) -- type: `object` - -Style of the arrow used to display links. - -Default value is: -```js -{ - color : 0xaaaaaa, - hoverColor : 0xaa5500, - outlineColor: 0x000000, - scale : [0.5, 2], -} -``` - -(The 3D model cannot be modified). - -#### `markerLatOffset` (markers+GPS mode only) -- type: `number` -- default: `-0.1` - -Vertical offset in radians applied to the markers to compensate for the viewer position above ground. - -#### `arrowPosition` (3d mode only) -- type: `'top' | 'bottom'` -- default: `'bottom'` - -Vertical position of the arrows. - - -## Methods - -#### `setNodes(nodes, [startNodeId])` (client mode only) - -Changes the nodes and display the first one (or the one designated by `startNodeId`). - -#### `setCurrentNode(nodeId)` - -Changes the current node. - - -## Events - -#### `node-changed(nodeId, data)` - -Triggered when the current node is changed. - -```js -virtualTourPlugin.on('node-changed', (e, nodeId, data) => { - console.log(`Current node is ${nodeId}`); - if (data.fromNode) { // other data are available - console.log(`Previous node was ${data.fromNode.id}`); - } -}); -``` diff --git a/docs/plugins/plugin-visible-range.md b/docs/plugins/plugin-visible-range.md deleted file mode 100644 index 4941dbf27..000000000 --- a/docs/plugins/plugin-visible-range.md +++ /dev/null @@ -1,131 +0,0 @@ -# VisibleRangePlugin - - - -> Locks visible longitude and/or latitude. - -This plugin is available in the core `photo-sphere-viewer` package in `dist/plugins/visible-range.js`. - -[[toc]] - - -## Usage - -The plugin allows to define `latitudeRange` and `longitudeRange` to lock to viewable zone. It affects manual moves and automatic rotation. - -```js -const viewer = new PhotoSphereViewer.Viewer({ - plugins: [ - [PhotoSphereViewer.VisibleRangePlugin, { - longitudeRange: [-Math.PI / 2, Math.PI / 2], - latitudeRange : [-Math.PI / 3, Math.PI / 3], - }], - ], -}); - -const visibleRangePlugin = viewer.getPlugin(PhotoSphereViewer.VisibleRangePlugin); - -visibleRangePlugin.setLongitudeRange(['0deg', '180deg']); -visibleRangePlugin.setLatitudeRange(null); -``` - -Alternatively, if `usePanoData` is set to `true`, the visible range is limited to the [cropped panorama data](../guide/adapters/equirectangular.md#cropped-panorama) provided to the viewer. - -## Example - -::: code-demo - -```yaml -title: PSV Visible Range Demo -resources: - - path: plugins/visible-range.js - imports: VisibleRangePlugin -``` - -```js -const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; - -let visibleRangePlugin; - -const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere-cropped.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - defaultZoomLvl: 30, - - navbar: [ - 'autorotate', - // custom buttons to clear and set the range - { - content : 'Clear range', - className: 'custom-button', - onClick : () => { - visibleRangePlugin.setLongitudeRange(null); - visibleRangePlugin.setLatitudeRange(null); - }, - }, - { - content : 'Set custom range', - className: 'custom-button', - onClick : () => { - visibleRangePlugin.setLongitudeRange([-Math.PI / 2, Math.PI / 2]); - visibleRangePlugin.setLatitudeRange([-Math.PI / 3, Math.PI / 3]); - }, - }, - { - content : 'Set range from panoData', - className: 'custom-button', - onClick : () => { - visibleRangePlugin.setRangesFromPanoData(); - }, - }, - 'caption', - 'fullscreen', - ], - - plugins: [ - [PhotoSphereViewer.VisibleRangePlugin, { - usePanoData: true, - }], - ], -}); - -visibleRangePlugin = viewer.getPlugin(PhotoSphereViewer.VisibleRangePlugin); -``` - -::: - - -## Configuration - -#### `longitudeRange` -- type: `double[]|string[]` -- default: `null` - -Visible longitude as two angles. - -#### `latitudeRange` -- type: `double[]|string[]` -- default: `null` - -Visible latitude as two angles. - -#### `usePanoData` -- type: `boolean` -- default: `false` - -Use cropped panorama data as visible range immediately after load. - - -## Methods - -#### `setLatitudeRange(range)` | `setLongitudeRange(range)` - -Change or remove the ranges. - -#### `setRangesFromPanoData()` - -Use cropped panorama data as visible range. diff --git a/docs/plugins/resolution.md b/docs/plugins/resolution.md new file mode 100644 index 000000000..0113211d2 --- /dev/null +++ b/docs/plugins/resolution.md @@ -0,0 +1,140 @@ +# ResolutionPlugin + +::: module modules/plugin__Resolution.html +Adds a button to choose between multiple resolutions of the panorama. **Requires the [Settings plugin](./settings.md).** + +This plugin is available in the `@photo-sphere-viewer/resolution-plugin` package. +::: + +[[toc]] + +::: warning +ResolutionPlugin is not compatible with GalleryPlugin. +::: + +## Usage + +Once enabled the plugin will add a new setting the user can use to change the resolution of the panorama. + +```js +const viewer = new PhotoSphereViewer.Viewer({ + plugins: [ + PhotoSphereViewer.SettingsPlugin, + [PhotoSphereViewer.ResolutionPlugin, { + defaultResolution: 'SD', + resolutions: [ + { + id: 'SD', + label: 'Small', + panorama: 'sphere_small.jpg', + }, + { + id: 'HD', + label: 'Normal', + panorama: 'sphere.jpg', + }, + ], + }], + ], +}); +``` + +## Example + +The following example provides two resolutions for the panorama, "small" is loaded by default. + +::: code-demo + +```yaml +title: PSV Resolution Demo +packages: + - name: settings-plugin + imports: SettingsPlugin + style: true + - name: resolution-plugin + imports: ResolutionPlugin +``` + +```js +const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; + +const viewer = new PhotoSphereViewer.Viewer({ + container: 'viewer', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + + plugins: [ + PhotoSphereViewer.SettingsPlugin, + [PhotoSphereViewer.ResolutionPlugin, { + defaultResolution: 'SD', + resolutions: [ + { + id: 'SD', + label: 'Small', + panorama: baseUrl + 'sphere-small.jpg', + }, + { + id: 'HD', + label: 'Normal', + panorama: baseUrl + 'sphere.jpg', + }, + ], + }], + ], +}); +``` + +::: + +## Configuration + +#### `resolutions` + +- type: `object[]` + +List of available resolutions. Each resolution consist of an object with the properties `id`, `label` and `panorama`. +Cubemaps are supported. + +#### `defaultResolution` + +- type: `string` + +The id of the default resolution to load. If not provided the first resolution will be used. + +::: warning +If a `panorama` is initially configured on the viewer, this setting is ignored. +::: + +#### `showBadge` + +- type: `boolean` +- default: `true` + +Show the resolution id as a badge on the settings button. + +#### `lang` + +- type: `object` +- default: + +```js +lang: { + resolution: 'Quality', +} +``` + +_Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ + +## Events + +#### `resolution-changed(id)` + +Triggered when the resolution is changed. + +```js +resolutionPlugin.addEventListener('resolution-changed', ({ id }) => { + console.log(`Current resolution: ${id}`); +}); +``` diff --git a/docs/plugins/plugin-settings.md b/docs/plugins/settings.md similarity index 53% rename from docs/plugins/plugin-settings.md rename to docs/plugins/settings.md index 36da3f936..431f184db 100644 --- a/docs/plugins/plugin-settings.md +++ b/docs/plugins/settings.md @@ -1,27 +1,23 @@ # SettingsPlugin - +::: module modules/plugin__Settings.html +This plugin does nothing on it's own but is required by other plugins. -> This plugin does nothing on it's own but is required by other plugins. - -This plugin is available in the core `photo-sphere-viewer` package in `dist/plugins/settings.js` and `dist/plugins/settings.css`. +This plugin is available in the `@photo-sphere-viewer/settings-plugin` package. **It has a stylesheet.** +::: [[toc]] - ## Usage Once enabled the plugin will add a new "Settings" button which other plugins can use to display various settings in the side panel. ```js const viewer = new PhotoSphereViewer.Viewer({ - plugins: [ - PhotoSphereViewer.SettingsPlugin, - ], + plugins: [PhotoSphereViewer.SettingsPlugin], }); ``` - ## Example The following example manually adds two settings. @@ -30,26 +26,24 @@ The following example manually adds two settings. ```yaml title: PSV Settings Demo -resources: - - path: plugins/settings.js - imports: SettingsPlugin - - path: plugins/settings.css +packages: + - name: settings-plugin + imports: SettingsPlugin + style: true ``` ```js const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; const viewer = new PhotoSphereViewer.Viewer({ - container: 'viewer', - panorama: baseUrl + 'sphere.jpg', - caption: 'Parc national du Mercantour © Damien Sorel', - loadingImg: baseUrl + 'loader.gif', - touchmoveTwoFingers: true, - mousewheelCtrlKey: true, - - plugins: [ - PhotoSphereViewer.SettingsPlugin, - ], + container: 'viewer', + panorama: baseUrl + 'sphere.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + + plugins: [PhotoSphereViewer.SettingsPlugin], }); const settings = viewer.getPlugin(PhotoSphereViewer.SettingsPlugin); @@ -58,30 +52,29 @@ let currentToggle = true; let currentOption = 'A'; settings.addSetting({ - id : 'custom-toggle-setting', - label : 'Toggle setting', - type : 'toggle', - active: () => currentToggle, - toggle: () => currentToggle = !currentToggle, + id: 'custom-toggle-setting', + label: 'Toggle setting', + type: 'toggle', + active: () => currentToggle, + toggle: () => (currentToggle = !currentToggle), }); settings.addSetting({ - id : 'custom-options-setting', - label : 'Options setting', - type : 'options', - current: () => currentOption, - options: () => ([ - { id: 'A', label: 'Option A' }, - { id: 'B', label: 'Option B' }, - ]), - apply : (option) => currentOption = option, - badge : () => currentOption, + id: 'custom-options-setting', + label: 'Options setting', + type: 'options', + current: () => currentOption, + options: () => [ + { id: 'A', label: 'Option A' }, + { id: 'B', label: 'Option B' }, + ], + apply: (option) => (currentOption = option), + badge: () => currentOption, }); ``` ::: - ## Adding a setting Registering a new setting is done by calling the `addSetting` on the plugin. There are currently two types of setting. @@ -94,11 +87,11 @@ This a setting which has only two values : `true` and `false`. It is required to let enabled = false; settings.addSetting({ - id : 'custom-toggle-setting', - label : 'Toggle setting', - type : 'toggle', - active: () => enabled, - toggle: () => enabled = !enabled, + id: 'custom-toggle-setting', + label: 'Toggle setting', + type: 'toggle', + active: () => enabled, + toggle: () => (enabled = !enabled), }); ``` @@ -110,19 +103,18 @@ This is a setting which has multiple available values (or options). It is requir let currentOption = 'A'; settings.addSetting({ - id : 'custom-options-setting', - label : 'Options setting', - type : 'options', - options: () => ([ - { id: 'A', label: 'Option A' }, - { id: 'B', label: 'Option B' }, - ]), - current: () => currentOption, - apply : (option) => currentOption = option, + id: 'custom-options-setting', + label: 'Options setting', + type: 'options', + options: () => [ + { id: 'A', label: 'Option A' }, + { id: 'B', label: 'Option B' }, + ], + current: () => currentOption, + apply: (option) => (currentOption = option), }); ``` - ## Button badge A setting can also have a `badge` function, which return value will be used as a badge on the settings button itself. **Only one setting can declare a badge.** @@ -134,55 +126,59 @@ settings.addSetting({ }); ``` - ## Configuration -#### `persist` -- type: `boolean` -- default: `false` +#### `persist` + +- type: `boolean` +- default: `false` Should the settings be persisted. The persistence storage can be configured. -#### `storage` -- type: +#### `storage` + +- type: + ```ts { get(settingId: string): boolean | string | Promise; set(settingId: string, value: boolean | string); } ``` -- default: LocalStorage with key `psvSettings` + +- default: LocalStorage with key `psvSettings` Custom storage solution, for example LocalForage, NgRx, HTTP service, etc. #### `lang` -- type: `object` -- default: + +- type: `object` +- default: + ```js lang: { - settings : 'Settings', + settings: 'Settings', } ``` _Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ - ## Events -#### `setting-changed(id, value)` +#### `setting-changed(settingId, settingValue)` Triggered when the resolution is changed. ```js -settingsPlugin.on('setting-changed', (e, id, value) => { - console.log(`${id}: ${value}`); +settingsPlugin.addEventListener('setting-changed', ({ settingId, settingValue }) => { + console.log(`${settingId}: ${settingValue}`); }); ``` - ## Buttons This plugin adds buttons to the default navbar: -- `settings` allows to open the settings panel + +- `settings` allows to open the settings panel If you use a [custom navbar](../guide/navbar.md) you will need to manually add the buttons to the list. diff --git a/docs/plugins/stereo.md b/docs/plugins/stereo.md new file mode 100644 index 000000000..813e69d28 --- /dev/null +++ b/docs/plugins/stereo.md @@ -0,0 +1,46 @@ +# StereoPlugin + +::: module modules/plugin__Stereo.html +Adds stereo view on mobile devices. **Requires the [Gyroscope plugin](./gyroscope.md).** + +This plugin is available in the `@photo-sphere-viewer/stereo-plugin` package. +::: + +## Usage + +Once enabled the plugin will add a new "Stereo view" button only shown when the gyroscope API is available. It uses the WakeLock API to prevent the display from dimming or shuting down. + +```js +const viewer = new PhotoSphereViewer.Viewer({ + plugins: [ + PhotoSphereViewer.GyroscopePlugin, + PhotoSphereViewer.StereoPlugin, + ], +}); +``` + +## Configuration + +#### `lang` + +- type: `object` +- default: + +```js +lang: { + stereo: 'Stereo view', + stereoNotification: 'Click anywhere to exit stereo view.', + pleaseRotate: 'Please rotate your device', + tapToContinue: '(or tap to continue)', +} +``` + +_Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ + +## Buttons + +This plugin adds buttons to the default navbar: + +- `stereo` allows to start the stereo view + +If you use a [custom navbar](../guide/navbar.md) you will need to manually add the buttons to the list. diff --git a/docs/plugins/video.md b/docs/plugins/video.md new file mode 100644 index 000000000..349289267 --- /dev/null +++ b/docs/plugins/video.md @@ -0,0 +1,221 @@ +# VideoPlugin + +::: module modules/plugin__Video.html +Adds controls to the video [adapters](../guide/adapters/). + +This plugin is available in the `@photo-sphere-viewer/video-plugin` package. **It has a stylesheet.** +::: + +[[toc]] + +## Usage + +To use this plugin you must also load one of the video adapters : [equirectangular](../guide/adapters/equirectangular-video.md) or [cubemap](../guide/adapters/cubemap-video.md). + +Once enabled it will add various elements to the viewer: + +- Play/pause button +- Volume button +- Time indicator in the navbar +- Progressbar above the navbar +- Play button in the center of the viewer + +It also supports advanced autorotate with timed `keypoints`. + +```js +const viewer = new PhotoSphereViewer.Viewer({ + adapter: PhotoSphereViewer.EquirectangularVideoAdapter, + panorama: { + source: 'path/video.mp4', + }, + plugins: [PhotoSphereViewer.VideoPlugin], +}); +``` + +## Example + +::: code-demo + +```yaml +title: PSV Video Demo +packages: + - name: equirectangular-video-adapter + imports: EquirectangularVideoAdapter + - name: video-plugin + imports: VideoPlugin + style: true + - name: autorotate-plugin + imports: AutorotatePlugin + - name: settings-plugin + imports: SettingsPlugin + style: true + - name: resolution-plugin + imports: ResolutionPlugin +``` + +```js +const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; + +const viewer = new PhotoSphereViewer.Viewer({ + container: 'viewer', + adapter: [PhotoSphereViewer.EquirectangularVideoAdapter, { + muted: true, + }], + caption: 'Ayutthaya © meetle', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + navbar: 'video autorotate caption settings fullscreen', + + plugins: [ + [PhotoSphereViewer.VideoPlugin, { + keypoints: [ + { time: 0, position: { yaw: 0, pitch: 0 } }, + { time: 5, position: { yaw: -Math.PI / 4, pitch: Math.PI / 8 } }, + { time: 10, position: { yaw: -Math.PI / 2, pitch: 0 } }, + { time: 15, position: { yaw: (-3 * Math.PI) / 4, pitch: -Math.PI / 8 } }, + { time: 20, position: { yaw: -Math.PI, pitch: 0 } }, + { time: 25, position: { yaw: (-5 * Math.PI) / 4, pitch: Math.PI / 8 } }, + { time: 30, position: { yaw: (-3 * Math.PI) / 2, pitch: 0 } }, + { time: 35, position: { yaw: (-7 * Math.PI) / 4, pitch: -Math.PI / 8 } }, + ], + }], + PhotoSphereViewer.AutorotatePlugin, + PhotoSphereViewer.SettingsPlugin, + [PhotoSphereViewer.ResolutionPlugin, { + defaultResolution: 'HD', + resolutions: [ + { + id: 'UHD', + label: 'Ultra high', + panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_UHD.mp4' }, + }, + { + id: 'FHD', + label: 'High', + panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_FHD.mp4' }, + }, + { + id: 'HD', + label: 'Standard', + panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_HD.mp4' }, + }, + { + id: 'SD', + label: 'Low', + panorama: { source: baseUrl + 'equirectangular-video/Ayutthaya_SD.mp4' }, + }, + ], + }], + ], +}); +``` + +::: + +## Configuration + +#### `keypoints` + +- type: `Array<{ position, time }>` + +Defines timed keypoints that will be used by the autorotate button. + +```js +keypoints: [ + { time: 0, position: { yaw: 0, pitch: 0 } }, + { time: 5.5, position: { yaw: 0.25, pitch: 0 } }, + { time: 12.8, position: { yaw: 0.3, pitch: -12 } }, +]; +``` + +::: warning +The usage of keypoints requires to load the [Autorotate plugin](./autorotate.md). +::: + +#### `progressbar` + +- type: `boolean` +- default: `true` + +Displays a progressbar on top of the navbar. + +#### `bigbutton` + +- type: `boolean` +- default: `true` + +Displays a big "play" button in the center of the viewer. + +#### `lang` + +- type: `object` +- default: + +```js +lang: { + videoPlay: 'Play/Pause', + videoVolume: 'Volume', +} +``` + +_Note: this option is not part of the plugin but is merged with the main [`lang`](../guide/config.md#lang) object._ + + +## Multi resolution + +You can offer multiple resolutions of your video with the [ResolutionPlugin](./resolution.md). + +```js +const viewer = new PhotoSphereViewer.Viewer({ + adapter: PhotoSphereViewer.EquirectangularVideoAdapter, + plugins: [ + PhotoSphereViewer.VideoPlugin, + PhotoSphereViewer.SettingsPlugin, + [PhotoSphereViewer.ResolutionPlugin, { + defaultResolution: 'FHD', + resolutions: [ + { + id: 'UHD', + label: 'Ultra high', + panorama: { source: 'path/video-uhd.mp4' }, + }, + { + id: 'FHD', + label: 'High', + panorama: { source: 'path/video-fhd.mp4' }, + }, + { + id: 'HD', + label: 'Standard', + panorama: { source: 'path/video-hd.mp4' }, + }, + ], + }], + ], +}); +``` + +## Events + +#### `play-pause(playing)` + +Triggered when the video starts playing or is paused. + +#### `volume-change(volume)` + +Triggered when the video volume changes. + +#### `progress(time, duration, progress)` + +Triggered when the video play progression changes. + +## Buttons + +This plugin adds buttons to the default navbar: + +- `videoPlay` allows to play/pause the video +- `videoVolume` allows to change the volume/mute the video +- `videoTime` shows the video time and duration (not a real button) + +If you use a [custom navbar](../guide/navbar.md) you will need to manually add the buttons to the list. diff --git a/docs/plugins/virtual-tour.md b/docs/plugins/virtual-tour.md new file mode 100644 index 000000000..d59b7ac96 --- /dev/null +++ b/docs/plugins/virtual-tour.md @@ -0,0 +1,474 @@ +# VirtualTourPlugin + +::: module modules/plugin__VirtualTour.html +Create virtual tours by linking multiple panoramas. + +This plugin is available in the `@photo-sphere-viewer/virtual-tour-plugin` package. **It has a stylesheet.** +::: + +[[toc]] + +## Usage + +The plugin allows to define `nodes` which contains a `panorama` and one or more `links` to other nodes. The links are represented with a 3D arrow (default) or using the [Markers plugin](./markers.md). + +There are two different ways to define the position of the links : the manual mode and the GPS mode. + +:::: tabs + +::: tab Manual mode +In manual mode each link must have `yaw`/`pitch` or `textureX`/`textureY` coordinates to be placed at the correct location on the panorama. This works exactly like the placement of markers. + +```js +const node = { + id: 'node-1', + panorama: '001.jpg', + links: [ + { + nodeId: 'node-2', + position: { textureX: 1500, textureY: 780 }, + }, + ], +}; +``` + +::: + +::: tab GPS mode +In GPS mode each node has positionning coordinates and the links are placed automatically. + +```js +const node = { + id: 'node-1', + panorama: '001.jpg', + gps: [-80.156479, 25.666725], // optional altitude as 3rd value + links: [ + { + nodeId: 'node-2', + gps: [-80.156168, 25.666623], // the position of the linked node must be provided here in server mode + }, + ], +}; +``` + +::: + +:::: + +The nodes can be provided all at once or asynchronously as the user navigates. + +:::: tabs + +::: tab Client mode +In client mode you must provide all `nodes` all at once, you can also change all the nodes with the `setNodes` method. + +```js +const nodes = [ + { id: 'node-1', panorama: '001.jpg', links: [{ nodeId: 'node-2', position: { textureX: 1500, textureY: 780 } }] }, + { id: 'node-2', panorama: '002.jpg', links: [{ nodeId: 'node-1', position: { textureX: 3000, textureY: 780 } }] }, +]; +``` + +::: + +::: tab Server mode +In server mode you provide the `getNode callbacks function which returns a Promise to load the data of a node. + +```js +getNode = async (nodeId) => { + const res = await fetch(`/api/nodes/${nodeId}`); + return await res.json(); +}; +``` + +::: + +:::: + +::: tip +If the [Gallery plugin](./gallery.md) is loaded, it will be configured with the list of nodes (client mode only). +::: + +## Example + +::: code-demo + +```yaml +title: PSV Virtual Tour Demo +packages: + - name: virtual-tour-plugin + imports: VirtualTourPlugin + style: true + - name: gallery-plugin + imports: GalleryPlugin + style: true + - name: markers-plugin + imports: MarkersPlugin + style: true +``` + +```js +const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; + +const viewer = new PhotoSphereViewer.Viewer({ + container: 'viewer', + caption: 'Cape Florida Light, Key Biscayne © Pixexid', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + defaultYaw: '130deg', + navbar: 'zoom move gallery caption fullscreen', + + plugins: [ + PhotoSphereViewer.MarkersPlugin, + [PhotoSphereViewer.GalleryPlugin, { + thumbnailSize: { width: 100, height: 100 }, + }], + [PhotoSphereViewer.VirtualTourPlugin, { + positionMode: 'gps', + renderMode: '3d', + }], + ], +}); + +const virtualTour = viewer.getPlugin(PhotoSphereViewer.VirtualTourPlugin); + +virtualTour.setNodes([ + { + id: '1', + panorama: baseUrl + 'tour/key-biscayne-1.jpg', + thumbnail: baseUrl + 'tour/key-biscayne-1-thumb.jpg', + name: 'One', + links: [{ nodeId: '2' }], + markers: [ + { + id: 'marker-1', + image: baseUrl + 'pictos/pin-red.png', + tooltip: 'Cape Florida Light, Key Biscayne', + size: { width: 32, height: 32 }, + anchor: 'bottom center', + position: { yaw: '105deg', pitch: '35deg' }, + }, + ], + gps: [-80.156479, 25.666725, 3], + panoData: { poseHeading: 327 }, + }, + { + id: '2', + panorama: baseUrl + 'tour/key-biscayne-2.jpg', + thumbnail: baseUrl + 'tour/key-biscayne-2-thumb.jpg', + name: 'Two', + links: [{ nodeId: '3' }, { nodeId: '1' }], + gps: [-80.156168, 25.666623, 3], + panoData: { poseHeading: 318 }, + }, + { + id: '3', + panorama: baseUrl + 'tour/key-biscayne-3.jpg', + thumbnail: baseUrl + 'tour/key-biscayne-3-thumb.jpg', + name: 'Three', + links: [{ nodeId: '4' }, { nodeId: '2' }, { nodeId: '5' }], + gps: [-80.155932, 25.666498, 5], + panoData: { poseHeading: 328 }, + }, + { + id: '4', + panorama: baseUrl + 'tour/key-biscayne-4.jpg', + thumbnail: baseUrl + 'tour/key-biscayne-4-thumb.jpg', + name: 'Four', + links: [{ nodeId: '3' }, { nodeId: '5' }], + gps: [-80.156089, 25.666357, 3], + panoData: { poseHeading: 78 }, + }, + { + id: '5', + panorama: baseUrl + 'tour/key-biscayne-5.jpg', + thumbnail: baseUrl + 'tour/key-biscayne-5-thumb.jpg', + name: 'Five', + links: [{ nodeId: '6' }, { nodeId: '3' }, { nodeId: '4' }], + gps: [-80.156292, 25.666446, 2], + panoData: { poseHeading: 190 }, + }, + { + id: '6', + panorama: baseUrl + 'tour/key-biscayne-6.jpg', + thumbnail: baseUrl + 'tour/key-biscayne-6-thumb.jpg', + name: 'Six', + links: [{ nodeId: '5' }, { nodeId: '7' }], + gps: [-80.156465, 25.666496, 2], + panoData: { poseHeading: 328 }, + }, + { + id: '7', + panorama: baseUrl + 'tour/key-biscayne-7.jpg', + thumbnail: baseUrl + 'tour/key-biscayne-7-thumb.jpg', + name: 'Seven', + links: [{ nodeId: '6' }], + gps: [-80.15707, 25.6665, 3], + panoData: { poseHeading: 250 }, + }, +], '2'); +``` + +::: + +## Nodes options + +#### `id` (required) + +- type: `string` + +Unique identifier of the node + +#### `panorama` (required) + +Refer to the main [config page](../guide/config.md#panorama-required). + +#### `links` (required in client mode) + +- type: `array` + +Definition of the links of this node. See bellow. + +#### `gps` (required in GPS mode) + +- type: `number[]` + +GPS coordinates of this node as an array of two or three values (`[longitude, latitude, altitude]`). + +::: warning Projection system +Only the [ESPG:4326 projection](https://epsg.io/4326) is supported. +::: + +#### `name` + +- type: `string` + +Short name of this node, used in links tooltips and the gallery. + +#### `caption` + +Replace the global caption. Refer to the main [config page](../guide/config.md#caption). + +#### `description` + +Replace the global description. Refer to the main [config page](../guide/config.md#description). + +#### `thumbnail` + +- type: `string` + +Thumbnail for the nodes list in the gallery. + +#### `markers` + +- type: `MarkerConfig[]` + +Additional markers displayed on this node, requires the [Markers plugin](./markers.md). + +#### `panoData` + +Refer to the main [config page](../guide/config.md#panodata). + +#### `sphereCorrection` + +Refer to the main [config page](../guide/config.md#spherecorrection). + +## Links options + +#### `nodeId` (required) + +- type: `string` + +Identifier of the target node. + +#### `position` (required in manual mode) + +- type: `{ yaw, pitch } | { textureX, textureY }` + +Position of the link in **spherical coordinates** (radians/degrees) or **texture coordinates** (pixels). + +#### `gps` (required in GPS+server mode) + +- type: `number[]` + +Overrides the GPS coordinates of the target node. + +#### `name` + +- type: `string` + +Overrides the tooltip content (defaults to the node's `name` property). + +#### `arrowStyle` (3d mode only) + +- type: `object` + +Overrides the global style of the arrow used to display the link. See global configuration for details. + +#### `markerStyle` (markers mode only) + +- type: `object` + +Overrides the global style of the marker used to display the link. See global configuration for details. + +## Configuration + +#### `dataMode` + +- type: `'client' | 'server'` +- default: `'client'` + +Configure how the nodes configuration is provided. + +#### `positionMode` + +- type: `'manual' | 'gps'` +- default: `'manual'` + +Configure how the links between nodes are positionned. + +#### `renderMode` + +- type: `'markers' | '3d'` +- default: `'3d'` + +How the links are displayed, `markers` requires the [Markers plugin](./markers.md). + +#### `nodes` (client mode only) + +- type: `array` + +Initial list of nodes. You can also call `setNodes` method later. + +#### `getNode(nodeId)` (required in server mode) + +- type: `function(nodeId: string) => Promise` + +Callback to load the configuration of a node. + +#### `startNodeId` + +- type: `string` + +Id of the initially loaded node. If empty the first node will be displayed. You can also call `setCurrentNode` method later. + +#### `preload` + +- type: `boolean | function(node: Node, link: NodeLink) => boolean` +- default: `false` + +Enable the preloading of linked nodes, can be a function that returns true or false for each link. + +#### `rotateSpeed` + +- type: `boolean | string | number` +- default: `20rpm` + +When a link is clicked, adds a animation to face it before actually changing the node. If `false` the viewer won't rotate at all and keep the current orientation. + +#### `transition` + +- type: `boolean | number` +- default: `1500` + +Duration of the transition between nodes. + +#### `linksOnCompass` + +- type: `boolean` +- default: `true` if markers render mode + +If the [Compass plugin](./compass.md) is enabled, displays the links on the compass. + +#### `markerStyle` (markers mode only) + +- type: `object` + +Style of the marker used to display links. + +Default value is: + +```js +{ + html : arrowIconSvg, // an SVG provided by the plugin + size : { width: 80, height: 80 }, + scale : [0.5, 2], + anchor : 'top center', + className: 'psv-virtual-tour__marker', + style : { + color: 'rgba(0, 208, 255, 0.8)', + }, +} +``` + +::: tip +If you want to use another marker type like `image` you must define `html: null` to remove the default value. + +```js +markerStyle: { + html : null, + image: 'path/to/image.png', +} +``` + +::: + +#### `arrowStyle` (3d mode only) + +- type: `object` + +Style of the arrow used to display links. + +Default value is: + +```js +{ + color : 0xaaaaaa, + hoverColor : 0xaa5500, + outlineColor: 0x000000, + scale : [0.5, 2], +} +``` + +(The 3D model cannot be modified). + +#### `markerVerticalOffset` (markers+GPS mode only) + +- type: `number` +- default: `-0.1` + +Vertical offset in radians applied to the markers to compensate for the viewer position above ground. + +#### `arrowPosition` (3d mode only) + +- type: `'top' | 'bottom'` +- default: `'bottom'` + +Vertical position of the arrows. + +## Methods + +#### `setNodes(nodes, [startNodeId])` (client mode only) + +Changes the nodes and display the first one (or the one designated by `startNodeId`). + +#### `setCurrentNode(nodeId)` + +Changes the current node. + +## Events + +#### `node-changed(nodeId, data)` + +Triggered when the current node is changed. + +```js +virtualTourPlugin.addEventListener('node-changed', ({ node, data }) => { + console.log(`Current node is ${node.id}`); + if (data.fromNode) { + // other data are available + console.log(`Previous node was ${data.fromNode.id}`); + } +}); +``` diff --git a/docs/plugins/visible-range.md b/docs/plugins/visible-range.md new file mode 100644 index 000000000..227c6b63d --- /dev/null +++ b/docs/plugins/visible-range.md @@ -0,0 +1,130 @@ +# VisibleRangePlugin + +::: module modules/plugin__VisibleRange.html +Locks the visible angles. + +This plugin is available in the `@photo-sphere-viewer/visible-range-plugin` package. +::: + +[[toc]] + +## Usage + +The plugin allows to define `horizontalRange` and `verticalRange` to lock to viewable zone. It affects manual moves and automatic rotation. + +```js +const viewer = new PhotoSphereViewer.Viewer({ + plugins: [ + [PhotoSphereViewer.VisibleRangePlugin, { + horizontalRange: [-Math.PI / 2, Math.PI / 2], + verticalRange: [-Math.PI / 3, Math.PI / 3], + }], + ], +}); + +const visibleRangePlugin = viewer.getPlugin(PhotoSphereViewer.VisibleRangePlugin); + +visibleRangePlugin.setHorizontalRange(['0deg', '180deg']); +visibleRangePlugin.setVerticalRange(null); +``` + +Alternatively, if `usePanoData` is set to `true`, the visible range is limited to the [cropped panorama data](../guide/adapters/equirectangular.md#cropped-panorama) provided to the viewer. + +## Example + +::: code-demo + +```yaml +title: PSV Visible Range Demo +packages: + - name: visible-range-plugin + imports: VisibleRangePlugin +``` + +```js +const baseUrl = 'https://photo-sphere-viewer-data.netlify.app/assets/'; + +let visibleRangePlugin; + +const viewer = new PhotoSphereViewer.Viewer({ + container: 'viewer', + panorama: baseUrl + 'sphere-cropped.jpg', + caption: 'Parc national du Mercantour © Damien Sorel', + loadingImg: baseUrl + 'loader.gif', + touchmoveTwoFingers: true, + mousewheelCtrlKey: true, + defaultZoomLvl: 30, + + navbar: [ + // custom buttons to clear and set the range + { + content: 'Clear range', + className: 'custom-button', + onClick: () => { + visibleRangePlugin.setHorizontalRange(null); + visibleRangePlugin.setVerticalRange(null); + }, + }, + { + content: 'Set custom range', + className: 'custom-button', + onClick: () => { + visibleRangePlugin.setHorizontalRange([-Math.PI / 2, Math.PI / 2]); + visibleRangePlugin.setVerticalRange([-Math.PI / 3, Math.PI / 3]); + }, + }, + { + content: 'Set range from panoData', + className: 'custom-button', + onClick: () => { + visibleRangePlugin.setRangesFromPanoData(); + }, + }, + 'caption', + 'fullscreen', + ], + + plugins: [ + [PhotoSphereViewer.VisibleRangePlugin, { + usePanoData: true, + }], + ], +}); + +visibleRangePlugin = viewer.getPlugin(PhotoSphereViewer.VisibleRangePlugin); +``` + +::: + +## Configuration + +#### `horizontalRange` + +- type: `double[]|string[]` +- default: `null` + +Visible horizontal range as two angles. + +#### `verticalRange` + +- type: `double[]|string[]` +- default: `null` + +Visible vertical range as two angles. + +#### `usePanoData` + +- type: `boolean` +- default: `false` + +Use cropped panorama data as visible range immediately after load. + +## Methods + +#### `setHorizontalRange(range)` | `setVerticalRange(range)` + +Change or remove the ranges. + +#### `setRangesFromPanoData()` + +Use cropped panorama data as visible range. diff --git a/docs/plugins/writing-a-plugin.md b/docs/plugins/writing-a-plugin.md index fbd097567..3100cc018 100644 --- a/docs/plugins/writing-a-plugin.md +++ b/docs/plugins/writing-a-plugin.md @@ -2,90 +2,121 @@ [[toc]] +::: tip Full featured example +You can find a complete example of plugin implementation in the [examples](https://github.com/mistic100/Photo-Sphere-Viewer/tree/dev/examples/custom-plugin) folder of the project. +::: + ## Syntax -The recommended way to create your own plugin is as an ES6 class extending `AbstractPlugin` provided by `photo-sphere-viewer` core package. +The recommended way to create your own plugin is as an ES6 class extending `AbstractPlugin` provided by `@photo-sphere-viewer/core` core package. **Requirements:** -- The plugin class **must** take a `PSV.Viewer` object as first parameter and pass it to the `super` constructor. -- It **must** have a `static id` property. -- It **must** implement the `init` method to perform initialization, like subscribing to events. -- It **must** implement the `destroy` method which is used to cleanup the plugin when the viewer is unloaded. -- The constructor **can** take an `options` object as second parameter. - -In the plugin you have access to `this.psv` which is the instance of the viewer, check the for more information. -Your plugin is also an [`EventEmitter`](https://github.com/mistic100/uEvent) with `on`, `off` and `trigger` methods. +- The plugin class **must** take a `Viewer` object as first parameter and pass it to the `super` constructor. +- It **must** have a `static id` property. +- It **must** implement the `init` method to perform initialization, like subscribing to events. +- It **must** implement the `destroy` method which is used to cleanup the plugin when the viewer is unloaded. +- The constructor **can** take an `config` object as second parameter. -```js -import { AbstractPlugin } from 'photo-sphere-viewer'; +In the plugin you have access to `this.viewer` which is the instance of the viewer, check the for more information. -export class PhotoSphereViewerCustomPlugin extends AbstractPlugin { +Your plugin is also an `EventTarget` with `addEventListener`, `removeEventListener` and `dispatchEvent` methods. - static id = 'custom-plugin'; +```js +import { AbstractPlugin } from '@photo-sphere-viewer/core'; - constructor(psv, options) { - super(psv); - } - - init() { - // do your initialisation logic here - } +export class CustomPlugin extends AbstractPlugin { + static id = 'custom-plugin'; - destroy() { - // do your cleanup logic here + constructor(viewer, config) { + super(viewer); + } - super.destroy(); - } + init() { + // do your initialisation logic here + } + destroy() { + // do your cleanup logic here + super.destroy(); + } } ``` Beside this main class, you can use any number of ES modules to split your code. +### Typed events -## Packaging +When developping in TypeScript it is handy to be able to strongly type each event you emit. That's why `AbstractPlugin` takes an optional template type representing the list of dispatchable events. All events must extends `TypedEvent` which is also a templated class to be able to type the `target` property. -The simplest way to package your plugin is by using [rollup.js](https://rollupjs.org) and [Babel](https://babeljs.io) with the following configuration: +```ts +/** + * Declare the events classes + */ +export class CustomPluginEvent extends TypedEvent { + static override readonly type = 'custom-event'; -```js -// rollup.config.js + constructor(public readonly value: boolean) { + super(CustomPluginEvent.type); + } +} -export default { - input : 'index.js', - output : { - file : 'browser.js', - name : 'PhotoSphereViewerCustomPlugin', - format : 'umd', - sourcemap: true, - globals : { - 'three' : 'THREE', - 'uevent' : 'uEvent', - 'photo-sphere-viewer': 'PhotoSphereViewer', - }, - }, - external: [ - 'three', - 'uevent', - 'photo-sphere-viewer', - ], - plugins : [ - require('rollup-plugin-babel')({ - exclude : 'node_modules/**', - babelHelpers: 'bundled', - }), - ], -}; +/** + * Declare the union of all events + */ +export type CustomPluginEvents = CustomPluginEvent; + +/** + * Provide the events type + */ +export class CustomPlugin extends AbstractPlugin { + /** + * Dispatch + */ + method() { + this.dispatch(new CustomPluginEvent(true)); + } +} + +/** + * Listen + */ +viewer.getPlugin(PhotoSphereViewerCustomPlugin) + .addEventListener(CustomPluginEvent.type, ({ value, target }) => { + // value is typed boolean + // target is typed PhotoSphereViewerCustomPlugin + }); ``` -```json -// .babelrc +## Packaging -{ - "presets": [ - "@babel/env" - ] -} +The simplest way to package your plugin is by using [rollup.js](https://rollupjs.org) with the following configuration: + +```js +export default { + input: 'src/index.js', + output: [ + { + file: 'dist/index.js', + format: 'umd', + name: 'PhotoSphereViewerCustomPlugin', + sourcemap: true, + globals: { + 'three': 'THREE', + '@photo-sphere-viewer/core': 'PhotoSphereViewer', + }, + }, + { + file: 'dist/index.module.js', + format: 'es', + sourcemap: true, + }, + ], + external: [ + 'three', + '@photo-sphere-viewer/core', + ], +}; ``` ### Stylesheets @@ -94,13 +125,10 @@ If your plugin requires custom CSS, import the stylesheet directly in your main ```js require('rollup-plugin-postcss')({ - extract : true, - sourceMap: true, - use : ['sass'], - plugins : [ - require('autoprefixer')({}), - ], -}) + extract: true, + sourceMap: true, + use: ['sass'], +}); ``` ## Buttons @@ -109,59 +137,61 @@ Your plugin may need to add a new button in the navbar. This section will descri ### Creating a button -Photo Sphere Viewer buttons **must** extend `AbstractButton`, check the for more information. +Photo Sphere Viewer buttons **must** extend `AbstractButton`, check the for more information. **Requirements:** -- The button class **must** take a `PSV.components.Navbar` object as first parameter and pass it to the `super` constructor. -- It **must** have a `static id` property. -- It **must** implement the `destroy` method which is used to cleanup the button when the viewer is unloaded. -- It **must** implement the `onClick` method to perform an action. -- It **should** have a `static icon` property containing a SVG. -- It **can** implement the `isSupported` method to inform the viewer if the action is possible depending on the environement. -- It **can** provide additional parameters to `super` : - - 2nd: a CSS class name applied to the button - - 3rd: a boolean indicating the button can be collapsed in the menu on small screens (default `false`) - - 4th: a boolean indicating the button can be activated with the keyboard (default `true`) + +- The button class **must** take a `Navbar` object as first parameter and pass it to the `super` constructor. +- It **must** have a `static id` property. +- It **must** implement the `destroy` method which is used to cleanup the button when the viewer is unloaded. +- It **must** implement the `onClick` method to perform an action. +- It **can** implement the `isSupported` method to inform the viewer if the action is possible depending on the environement. +- It **must** provide the button configuration to `super` : + - `className` : CSS class name applied to the button + - `icon` : SVG of the icon + - `collapsable` : indicates the button can be collapsed in the menu on small screens + - `tabbable` : indicates the button can be activated with the keyboard ```js -import { AbstractButton } from 'photo-sphere-viewer'; +import { AbstractButton } from '@photo-sphere-viewer/core'; export class CustomButton extends AbstractButton { + static id = 'custom-button'; + + constructor(navbar) { + super(navbar, { + className: 'custom-button-class', + icon: '...', + collapsable: true, + tabbable: true, + }); + + // do your initialisation logic here + // you will probably need the instance of your plugin + this.plugin = this.viewer.getPlugin('custom-plugin'); + } - static id = 'custom-button'; - static icon = customIcon; - - constructor(navbar) { - super(navbar, 'custom-button-class', true, true); - - // do your initialisation logic here - // you will probably need the instance of your plugin - this.plugin = this.psv.getPlugin('custom-plugin'); - } - - destroy() { - // do your cleanup logic here - - super.destroy(); - } - - isSupported() { - return !!this.plugin; - } + destroy() { + // do your cleanup logic here + super.destroy(); + } - onClick() { - this.plugin.doSomething(); - } + isSupported() { + return !!this.plugin; + } + onClick() { + this.plugin.doSomething(); + } } ``` ### Registering the button -In your main plugin file call `registerButton` in your main plugin file. This will only make the button available but not display it by default, the user will have to declare it in its `navbar` configuration. +In your main plugin file call `registerButton`. This will only make the button available but not display it by default, the user will have to declare it in its `navbar` configuration. ```js -import { registerButton } from 'photo-sphere-viewer'; +import { registerButton } from '@photo-sphere-viewer/core'; import { CustomButton } from './CustomButton'; registerButton(CustomButton); @@ -173,18 +203,14 @@ If your button uses an icon, it is recommended to use an external SVG and bundle ```js require('rollup-plugin-string').string({ - include: [ - 'icons/*.svg', - ], -}) + include: ['**/*.svg'], +}); ``` This allows to get SVG files as string with `import`. ```js -import customIcon from './icons/custom.svg'; - -static icon = customIcon; +import iconContent from './icon.svg'; ``` ::: tip Icon color @@ -193,67 +219,67 @@ To be correctly displayed in the navbar, the icon must use `fill="currentColor"` ## Viewer settings -A plugin can expose one or more settings to the viewer by using the [Settings plugin](./plugin-settings.md). +A plugin can expose one or more settings to the viewer by using the Settings plugin. -This is done by requiring the settings plugin and calling the `addSetting` method. Consult the [Settings plugin](./plugin-settings.md) page for more information. +This is done by requiring the settings plugin and calling the `addSetting` method. Consult the [Settings plugin](./settings.md) page for more information. ```js -export default class PhotoSphereViewerCustomPlugin extends AbstractPlugin { - - constructor(psv) { - super(psv); - - /** - * @type {PSV.plugins.SettingsPlugin} - */ - this.settings = null; - } - - init() { - this.settings = this.psv.getPlugin('settings'); - - // the user may choose to not import the Settings plugin - // you may choose to make it a requirement by throwing an error... - if (this.settings) { - this.settings.addSetting({ - id : 'custom-setting', - type : 'toggle', - label : 'Custom setting', - active: () => this.isActive(), - toggle: () => this.toggle(), - }); +export default class CustomPlugin extends AbstractPlugin { + constructor(viewer) { + super(viewer); + + /** + * @type {SettingsPlugin} + */ + this.settings = null; } - } - destroy() { - if (this.settings) { - this.settings.removeSetting('custom-setting'); - delete this.settings; + init() { + this.settings = this.viewer.getPlugin('settings'); + + // the user may choose to not import the Settings plugin + // you may choose to make it a requirement by throwing an error... + if (this.settings) { + this.settings.addSetting({ + id: 'custom-setting', + type: 'toggle', + label: 'Custom setting', + active: () => this.isActive(), + toggle: () => this.toggle(), + }); + } } - super.destroy(); - } + destroy() { + if (this.settings) { + this.settings.removeSetting('custom-setting'); + delete this.settings; + } + super.destroy(); + } } ``` - ## Naming and publishing -If you intend to publish your plugin on npmjs.org please respect the folowing naming: +If you intend to publish your plugin on npmjs.org please respect the following naming: -- class name and export name (in rollup config file) : `PhotoSphereViewer[[Name]]Plugin` -- NPM package name : `photo-sphere-viewer-[[name]]-plugin` +- class name : `[[Name]]Plugin` +- export name (in rollup config file) : `PhotoSphereViewer[[Name]]Plugin` +- NPM package name : `photo-sphere-viewer-[[name]]-plugin` -Your `package.json` must be properly configured to allow application bundlers to get the right file, and `photo-sphere-viewer` must be declared as peer dependency. +Your `package.json` must be properly configured to allow application bundlers to get the right file, and `@photo-sphere-viewer/core` must be declared as dependency. ```json { - "name": "photo-sphere-viewer-custom-plugin", - "version": "1.0.0", - "main": "browser.js", - "peerDependencies": { - "photo-sphere-viewer": "^4.0.0" - } + "name": "photo-sphere-viewer-custom-plugin", + "version": "1.0.0", + "main": "index.js", + "module": "index.module.js", + "style": "index.css", + "dependencies": { + "@photo-sphere-viewer/core": "^5.0.0" + } } ``` diff --git a/example/assets/photosphere-logo.gif b/example/assets/photosphere-logo.gif deleted file mode 100644 index 199ebdd87..000000000 Binary files a/example/assets/photosphere-logo.gif and /dev/null differ diff --git a/example/assets/pin-blue.png b/example/assets/pin-blue.png deleted file mode 100644 index 061bcdd18..000000000 Binary files a/example/assets/pin-blue.png and /dev/null differ diff --git a/example/assets/pin-red.png b/example/assets/pin-red.png deleted file mode 100644 index e6b4328dc..000000000 Binary files a/example/assets/pin-red.png and /dev/null differ diff --git a/example/assets/target.png b/example/assets/target.png deleted file mode 100644 index 5cc17283b..000000000 Binary files a/example/assets/target.png and /dev/null differ diff --git a/example/assets/tent.png b/example/assets/tent.png deleted file mode 100644 index 3af7db91d..000000000 Binary files a/example/assets/tent.png and /dev/null differ diff --git a/example/components.html b/example/components.html deleted file mode 100644 index 397371b84..000000000 --- a/example/components.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - PhotoSphereViewer - components demo - - - - - - -
- - - - - - - - - - - - diff --git a/example/cubemap-overlay.html b/example/cubemap-overlay.html deleted file mode 100644 index 77ef48111..000000000 --- a/example/cubemap-overlay.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - PhotoSphereViewer - overlay demo - - - - - - -
- - - - - - - - - - diff --git a/example/cubemap-tiles.html b/example/cubemap-tiles.html deleted file mode 100644 index 94ed3824d..000000000 --- a/example/cubemap-tiles.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - PhotoSphereViewer - cubemap tiles demo - - - - - - -
- - - - - - - - - - - diff --git a/example/cubemap-video.html b/example/cubemap-video.html deleted file mode 100644 index 387135507..000000000 --- a/example/cubemap-video.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - PhotoSphereViewer - cubemap video demo - - - - - - - - -
- - - - - - - - - - - - diff --git a/example/cubemap.html b/example/cubemap.html deleted file mode 100644 index 8a1bba84b..000000000 --- a/example/cubemap.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - PhotoSphereViewer - cubemap demo - - - - - - -
- - - - - - - - - diff --git a/example/cubemap/nx.jpg b/example/cubemap/nx.jpg deleted file mode 100644 index eac48f8d4..000000000 Binary files a/example/cubemap/nx.jpg and /dev/null differ diff --git a/example/cubemap/ny.jpg b/example/cubemap/ny.jpg deleted file mode 100644 index 8de23439d..000000000 Binary files a/example/cubemap/ny.jpg and /dev/null differ diff --git a/example/cubemap/nz.jpg b/example/cubemap/nz.jpg deleted file mode 100644 index 8ebb3e423..000000000 Binary files a/example/cubemap/nz.jpg and /dev/null differ diff --git a/example/cubemap/px.jpg b/example/cubemap/px.jpg deleted file mode 100644 index 8e880ab66..000000000 Binary files a/example/cubemap/px.jpg and /dev/null differ diff --git a/example/cubemap/py.jpg b/example/cubemap/py.jpg deleted file mode 100644 index 479e65ce6..000000000 Binary files a/example/cubemap/py.jpg and /dev/null differ diff --git a/example/cubemap/pz.jpg b/example/cubemap/pz.jpg deleted file mode 100644 index 7ea0d3ed0..000000000 Binary files a/example/cubemap/pz.jpg and /dev/null differ diff --git a/example/equirectangular-overlay.html b/example/equirectangular-overlay.html deleted file mode 100644 index 6dc727a7c..000000000 --- a/example/equirectangular-overlay.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - PhotoSphereViewer - overlay demo - - - - - - -
- - - - - - - - - diff --git a/example/equirectangular-tiles.html b/example/equirectangular-tiles.html deleted file mode 100644 index bd723c705..000000000 --- a/example/equirectangular-tiles.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - PhotoSphereViewer - equirectangular tiles demo - - - - - - -
- - - - - - - - - - diff --git a/example/equirectangular-video.html b/example/equirectangular-video.html deleted file mode 100644 index b5e62f659..000000000 --- a/example/equirectangular-video.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - PhotoSphereViewer - equirectangular video demo - - - - - - - - - -
- - - - - - - - - - - - - diff --git a/example/equirectangular.html b/example/equirectangular.html deleted file mode 100644 index f71e1f2d2..000000000 --- a/example/equirectangular.html +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - PhotoSphereViewer - equirectangular demo - - - - - - -
- - - - - - - - - - diff --git a/example/index.html b/example/index.html deleted file mode 100644 index 590be4258..000000000 --- a/example/index.html +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - PhotoSphereViewer - Demos - - - - - - - - - - diff --git a/example/little-planet.html b/example/little-planet.html deleted file mode 100644 index 29eb49d59..000000000 --- a/example/little-planet.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - PhotoSphereViewer - little planet demo - - - - - - -
- - - - - - - - - - diff --git a/example/plugin-autorotate-keypoints.html b/example/plugin-autorotate-keypoints.html deleted file mode 100644 index b2d3f5c8b..000000000 --- a/example/plugin-autorotate-keypoints.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - PhotoSphereViewer - autorotate keypoints demo - - - - - - - -
- - - - - - - - - - diff --git a/example/plugin-compass.html b/example/plugin-compass.html deleted file mode 100644 index 31fd3d123..000000000 --- a/example/plugin-compass.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - PhotoSphereViewer - compass demo - - - - - - - - -
- - - - - - - - - - diff --git a/example/plugin-gallery.html b/example/plugin-gallery.html deleted file mode 100644 index 498145e2d..000000000 --- a/example/plugin-gallery.html +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - PhotoSphereViewer - gallery demo - - - - - - - -
- - - - - - - - - diff --git a/example/plugin-gyroscope-stereo.html b/example/plugin-gyroscope-stereo.html deleted file mode 100644 index 3d30f7c38..000000000 --- a/example/plugin-gyroscope-stereo.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - PhotoSphereViewer - gyroscope & stereo demo - - - - - - -
- - - - - - - - - - - diff --git a/example/plugin-markers-cubemap.html b/example/plugin-markers-cubemap.html deleted file mode 100644 index e21e4e7b2..000000000 --- a/example/plugin-markers-cubemap.html +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - PhotoSphereViewer - markers demo (cubemap) - - - - - - - -
- - - - - - - - - - diff --git a/example/plugin-markers.html b/example/plugin-markers.html deleted file mode 100644 index b5cc58436..000000000 --- a/example/plugin-markers.html +++ /dev/null @@ -1,518 +0,0 @@ - - - - - - PhotoSphereViewer - markers demo - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/plugin-resolution.html b/example/plugin-resolution.html deleted file mode 100644 index c571e4689..000000000 --- a/example/plugin-resolution.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - PhotoSphereViewer - resolution demo - - - - - - - -
- - - - - - - - - - diff --git a/example/plugin-settings.html b/example/plugin-settings.html deleted file mode 100644 index d7f15e843..000000000 --- a/example/plugin-settings.html +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - PhotoSphereViewer - settings demo - - - - - - - -
- - - - - - - - - diff --git a/example/plugin-virtual-tour.html b/example/plugin-virtual-tour.html deleted file mode 100644 index 70e005f77..000000000 --- a/example/plugin-virtual-tour.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - PhotoSphereViewer - virtual tour demo - - - - - - - - - - -
- - - - - - - - - - - - diff --git a/example/plugin-visible-range.html b/example/plugin-visible-range.html deleted file mode 100644 index ba845bbf8..000000000 --- a/example/plugin-visible-range.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - PhotoSphereViewer - visible range demo - - - - - - -
- - - - - - - - - diff --git a/example/sphere.jpg b/example/sphere.jpg deleted file mode 100644 index db232847e..000000000 Binary files a/example/sphere.jpg and /dev/null differ diff --git a/example/sphere_small.jpg b/example/sphere_small.jpg deleted file mode 100644 index 4b86fca31..000000000 Binary files a/example/sphere_small.jpg and /dev/null differ diff --git a/example/style.css b/example/style.css deleted file mode 100644 index 0595023fb..000000000 --- a/example/style.css +++ /dev/null @@ -1,17 +0,0 @@ -html, body { - width: 100%; - height: 100%; - margin: 0; - padding: 0; - font-family: sans-serif; -} - -#photosphere { - width: 100%; - height: 100%; -} - -.psv-button.custom-button { - font-size: 22px; - line-height: 20px; -} diff --git a/example/test-cubemap/1.png b/example/test-cubemap/1.png deleted file mode 100644 index e8808d395..000000000 Binary files a/example/test-cubemap/1.png and /dev/null differ diff --git a/example/test-cubemap/2.png b/example/test-cubemap/2.png deleted file mode 100644 index ee117d07d..000000000 Binary files a/example/test-cubemap/2.png and /dev/null differ diff --git a/example/test-cubemap/3.png b/example/test-cubemap/3.png deleted file mode 100644 index 0546ded44..000000000 Binary files a/example/test-cubemap/3.png and /dev/null differ diff --git a/example/test-cubemap/4.png b/example/test-cubemap/4.png deleted file mode 100644 index e12bd14bb..000000000 Binary files a/example/test-cubemap/4.png and /dev/null differ diff --git a/example/test-cubemap/5.png b/example/test-cubemap/5.png deleted file mode 100644 index 22a236148..000000000 Binary files a/example/test-cubemap/5.png and /dev/null differ diff --git a/example/test-cubemap/6.png b/example/test-cubemap/6.png deleted file mode 100644 index 15a7ad7a6..000000000 Binary files a/example/test-cubemap/6.png and /dev/null differ diff --git a/example/test-sphere.jpg b/example/test-sphere.jpg deleted file mode 100644 index 3cbc9ab07..000000000 Binary files a/example/test-sphere.jpg and /dev/null differ diff --git a/examples/components.html b/examples/components.html new file mode 100644 index 000000000..e07557a50 --- /dev/null +++ b/examples/components.html @@ -0,0 +1,112 @@ + + + + + + PhotoSphereViewer - components demo + + + + + +
+ + + + + + + + + + + diff --git a/examples/cubemap-overlay.html b/examples/cubemap-overlay.html new file mode 100644 index 000000000..4907b4e9b --- /dev/null +++ b/examples/cubemap-overlay.html @@ -0,0 +1,99 @@ + + + + + + PhotoSphereViewer - overlay demo + + + + + +
+ + + + + + + + + diff --git a/examples/cubemap-tiles.html b/examples/cubemap-tiles.html new file mode 100644 index 000000000..50205022b --- /dev/null +++ b/examples/cubemap-tiles.html @@ -0,0 +1,124 @@ + + + + + + PhotoSphereViewer - cubemap tiles demo + + + + + +
+ + + + + + + + + + diff --git a/examples/cubemap-video.html b/examples/cubemap-video.html new file mode 100644 index 000000000..4100c8007 --- /dev/null +++ b/examples/cubemap-video.html @@ -0,0 +1,66 @@ + + + + + + PhotoSphereViewer - cubemap video demo + + + + + + + +
+ + + + + + + + + + + diff --git a/examples/cubemap.html b/examples/cubemap.html new file mode 100644 index 000000000..a354b50e9 --- /dev/null +++ b/examples/cubemap.html @@ -0,0 +1,93 @@ + + + + + + PhotoSphereViewer - cubemap demo + + + + + +
+ + + + + + + + diff --git a/examples/custom-plugin/.gitignore b/examples/custom-plugin/.gitignore new file mode 100644 index 000000000..cf762fe6b --- /dev/null +++ b/examples/custom-plugin/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +yarn.lock diff --git a/examples/custom-plugin/README.md b/examples/custom-plugin/README.md new file mode 100644 index 000000000..7f2344511 --- /dev/null +++ b/examples/custom-plugin/README.md @@ -0,0 +1,11 @@ +# Photo Sphere Viewer Custom Plugin + +This is an example of how to implement, build and publish a custom plugin for Photo Sphere Viewer. + +Features : +- TypeScript +- SCSS +- custom button with SVG icon +- custom events + +For more informationn check https://photo-sphere-viewer.js.org/plugins/writing-a-plugin.html diff --git a/examples/custom-plugin/package.json b/examples/custom-plugin/package.json new file mode 100644 index 000000000..a0349f352 --- /dev/null +++ b/examples/custom-plugin/package.json @@ -0,0 +1,22 @@ +{ + "name": "photo-sphere-viewer-custom-plugin", + "version": "1.0.0", + "description": "Photo Sphere Viewer custom plugin example", + "license": "MIT", + "author": "mistic100", + "scripts": { + "build": "rollup --config rollup.config.mjs" + }, + "dependencies": { + "@photo-sphere-viewer/core": "file:../../packages/core/dist" + }, + "devDependencies": { + "rollup": "^3.7.2", + "rollup-plugin-generate-package-json": "^3.2.0", + "rollup-plugin-postcss": "^4.0.2", + "rollup-plugin-string": "^3.0.0", + "rollup-plugin-ts": "^3.0.2", + "sass": "^1.56.2", + "typescript": "^4.9.3" + } +} diff --git a/examples/custom-plugin/rollup.config.mjs b/examples/custom-plugin/rollup.config.mjs new file mode 100644 index 000000000..0d7479dce --- /dev/null +++ b/examples/custom-plugin/rollup.config.mjs @@ -0,0 +1,58 @@ +import generatePackageJson from 'rollup-plugin-generate-package-json'; +import postcss from 'rollup-plugin-postcss'; +import { string } from 'rollup-plugin-string'; +import ts from 'rollup-plugin-ts'; + +export default { + input: 'src/index.ts', + output: [ + { + file: 'dist/index.js', + format: 'umd', + name: 'PhotoSphereViewerCustomPlugin', + sourcemap: true, + globals: { + 'three': 'THREE', + '@photo-sphere-viewer/core': 'PhotoSphereViewer', + }, + }, + { + file: 'dist/index.module.js', + format: 'es', + sourcemap: true, + }, + ], + external: [ + 'three', + '@photo-sphere-viewer/core', + ], + plugins: [ + ts(), + postcss({ + extract: 'index.css', + sourceMap: true, + use: ['sass'], + }), + string({ + include: ['**/*.svg'], + }), + generatePackageJson({ + baseContents: (pkg) => { + pkg = { + ...pkg, + main: "index.js", + module: "index.module.js", + types: "index.d.ts", + style: "index.css", + }; + delete pkg.scripts; + delete pkg.devDependencies; + return pkg; + }, + // this is only necessary for this demo, to override the "file" dependency + additionalDependencies: { + "@photo-sphere-viewer/core": "^5.0.0" + }, + }), + ], +}; \ No newline at end of file diff --git a/examples/custom-plugin/src/CustomButton.ts b/examples/custom-plugin/src/CustomButton.ts new file mode 100644 index 000000000..4feccceaa --- /dev/null +++ b/examples/custom-plugin/src/CustomButton.ts @@ -0,0 +1,36 @@ +import type { Navbar } from '@photo-sphere-viewer/core'; +import { AbstractButton } from '@photo-sphere-viewer/core'; +import type { CustomPlugin } from './CustomPlugin'; +import icon from './icon.svg'; + +export class CustomButton extends AbstractButton { + static override readonly id = 'custom-button'; + + private plugin: CustomPlugin; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'custom-plugin__button', + icon: icon, + collapsable: true, + tabbable: true, + }); + + // do your initialisation logic here + // you will probably need the instance of your plugin + this.plugin = this.viewer.getPlugin('custom-plugin'); + } + + override destroy() { + // do your cleanup logic here + super.destroy(); + } + + override isSupported() { + return !!this.plugin; + } + + onClick() { + this.plugin.doSomething(); + } +} \ No newline at end of file diff --git a/examples/custom-plugin/src/CustomPlugin.ts b/examples/custom-plugin/src/CustomPlugin.ts new file mode 100644 index 000000000..8c608c715 --- /dev/null +++ b/examples/custom-plugin/src/CustomPlugin.ts @@ -0,0 +1,26 @@ +import type { Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin } from '@photo-sphere-viewer/core'; +import { CustomPluginEvent, CustomPluginEvents } from './events'; +import { CustomPluginConfig } from './model'; + +export class CustomPlugin extends AbstractPlugin { + static override readonly id = 'custom-plugin'; + + constructor(viewer: Viewer, private config: CustomPluginConfig) { + super(viewer); + } + + override init() { + // do your initialisation logic here + console.log(this.config.foo); + } + + override destroy() { + // do your cleanup logic here + super.destroy(); + } + + doSomething() { + this.dispatchEvent(new CustomPluginEvent(true)); + } +} diff --git a/examples/custom-plugin/src/events.ts b/examples/custom-plugin/src/events.ts new file mode 100644 index 000000000..5c6234a00 --- /dev/null +++ b/examples/custom-plugin/src/events.ts @@ -0,0 +1,15 @@ +import { TypedEvent } from '@photo-sphere-viewer/core'; +import type { CustomPlugin } from './CustomPlugin'; + +/** + * @event Triggered when something happens + */ + export class CustomPluginEvent extends TypedEvent { + static override readonly type = 'custom-event'; + + constructor(public readonly value: boolean) { + super(CustomPluginEvent.type); + } +} + +export type CustomPluginEvents = CustomPluginEvent; diff --git a/examples/custom-plugin/src/icon.svg b/examples/custom-plugin/src/icon.svg new file mode 100644 index 000000000..bc7521973 --- /dev/null +++ b/examples/custom-plugin/src/icon.svg @@ -0,0 +1 @@ + diff --git a/examples/custom-plugin/src/index.ts b/examples/custom-plugin/src/index.ts new file mode 100644 index 000000000..ac7320a61 --- /dev/null +++ b/examples/custom-plugin/src/index.ts @@ -0,0 +1,11 @@ +import { registerButton } from '@photo-sphere-viewer/core'; +import { CustomButton } from './CustomButton'; + +registerButton(CustomButton); + +export * from './model'; +export * from './events'; +export * from './CustomPlugin'; + +/** @internal */ +import './style.scss'; diff --git a/examples/custom-plugin/src/model.ts b/examples/custom-plugin/src/model.ts new file mode 100644 index 000000000..09414362d --- /dev/null +++ b/examples/custom-plugin/src/model.ts @@ -0,0 +1,3 @@ +export type CustomPluginConfig = { + foo: string; +}; diff --git a/examples/custom-plugin/src/style.scss b/examples/custom-plugin/src/style.scss new file mode 100644 index 000000000..6c04a8611 --- /dev/null +++ b/examples/custom-plugin/src/style.scss @@ -0,0 +1,5 @@ +.custom-plugin { + &__button { + color: darkred; + } +} diff --git a/examples/custom-plugin/tsconfig.json b/examples/custom-plugin/tsconfig.json new file mode 100644 index 000000000..36f0f9a3d --- /dev/null +++ b/examples/custom-plugin/tsconfig.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "rootDir": ".", + "baseUrl": "./", + "target": "es2021", + "composite": false, + "declaration": true, + "declarationMap": false, + "inlineSources": true, + "stripInternal": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "moduleResolution": "node", + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "strictNullChecks": false, + "lib": ["es2021", "dom"] + }, + "include": ["src/**/*.ts", "typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/examples/custom-plugin/typings.d.ts b/examples/custom-plugin/typings.d.ts new file mode 100644 index 000000000..b409c17a8 --- /dev/null +++ b/examples/custom-plugin/typings.d.ts @@ -0,0 +1,9 @@ +declare module '*.svg' { + const content: string; + export default content; +} + +declare module '*.scss' { + const content: any; + export default content; +} diff --git a/examples/equirectangular-overlay.html b/examples/equirectangular-overlay.html new file mode 100644 index 000000000..65897782e --- /dev/null +++ b/examples/equirectangular-overlay.html @@ -0,0 +1,64 @@ + + + + + + PhotoSphereViewer - overlay demo + + + + + +
+ + + + + + + + diff --git a/examples/equirectangular-tiles.html b/examples/equirectangular-tiles.html new file mode 100644 index 000000000..470a6de20 --- /dev/null +++ b/examples/equirectangular-tiles.html @@ -0,0 +1,106 @@ + + + + + + PhotoSphereViewer - equirectangular tiles demo + + + + + +
+ + + + + + + + + diff --git a/examples/equirectangular-video.html b/examples/equirectangular-video.html new file mode 100644 index 000000000..d5aef5f05 --- /dev/null +++ b/examples/equirectangular-video.html @@ -0,0 +1,85 @@ + + + + + + PhotoSphereViewer - equirectangular video demo + + + + + + + + +
+ + + + + + + + + + + + + diff --git a/examples/equirectangular.html b/examples/equirectangular.html new file mode 100644 index 000000000..d2c28f697 --- /dev/null +++ b/examples/equirectangular.html @@ -0,0 +1,110 @@ + + + + + + PhotoSphereViewer - equirectangular demo + + + + + +
+ + + + + + + + + diff --git a/examples/index.html b/examples/index.html new file mode 100644 index 000000000..12cfb4f57 --- /dev/null +++ b/examples/index.html @@ -0,0 +1,104 @@ + + + + + + PhotoSphereViewer - Demos + + + + + + + diff --git a/examples/little-planet.html b/examples/little-planet.html new file mode 100644 index 000000000..f3d4d4574 --- /dev/null +++ b/examples/little-planet.html @@ -0,0 +1,79 @@ + + + + + + PhotoSphereViewer - little planet demo + + + + + +
+ + + + + + + + diff --git a/examples/plugin-autorotate.html b/examples/plugin-autorotate.html new file mode 100644 index 000000000..03300d3f4 --- /dev/null +++ b/examples/plugin-autorotate.html @@ -0,0 +1,91 @@ + + + + + + PhotoSphereViewer - autorotate demo + + + + + + +
+ + + + + + + + + diff --git a/examples/plugin-compass.html b/examples/plugin-compass.html new file mode 100644 index 000000000..c028b0bfc --- /dev/null +++ b/examples/plugin-compass.html @@ -0,0 +1,105 @@ + + + + + + PhotoSphereViewer - compass demo + + + + + + + +
+ + + + + + + + + diff --git a/examples/plugin-gallery.html b/examples/plugin-gallery.html new file mode 100644 index 000000000..dc407e92a --- /dev/null +++ b/examples/plugin-gallery.html @@ -0,0 +1,121 @@ + + + + + + PhotoSphereViewer - gallery demo + + + + + + +
+ + + + + + + + diff --git a/examples/plugin-gyroscope-stereo.html b/examples/plugin-gyroscope-stereo.html new file mode 100644 index 000000000..82f0023ca --- /dev/null +++ b/examples/plugin-gyroscope-stereo.html @@ -0,0 +1,37 @@ + + + + + + PhotoSphereViewer - gyroscope & stereo demo + + + + + +
+ + + + + + + + + diff --git a/examples/plugin-markers-cubemap.html b/examples/plugin-markers-cubemap.html new file mode 100644 index 000000000..dbb5ffeca --- /dev/null +++ b/examples/plugin-markers-cubemap.html @@ -0,0 +1,96 @@ + + + + + + PhotoSphereViewer - markers demo (cubemap) + + + + + + +
+ + + + + + + + + diff --git a/examples/plugin-markers.html b/examples/plugin-markers.html new file mode 100644 index 000000000..0d3f462bc --- /dev/null +++ b/examples/plugin-markers.html @@ -0,0 +1,486 @@ + + + + + + PhotoSphereViewer - markers demo + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/plugin-resolution.html b/examples/plugin-resolution.html new file mode 100644 index 000000000..d8e46b690 --- /dev/null +++ b/examples/plugin-resolution.html @@ -0,0 +1,49 @@ + + + + + + PhotoSphereViewer - resolution demo + + + + + + +
+ + + + + + + + + diff --git a/examples/plugin-settings.html b/examples/plugin-settings.html new file mode 100644 index 000000000..8ed0ad521 --- /dev/null +++ b/examples/plugin-settings.html @@ -0,0 +1,73 @@ + + + + + + PhotoSphereViewer - settings demo + + + + + + +
+ + + + + + + + diff --git a/examples/plugin-virtual-tour.html b/examples/plugin-virtual-tour.html new file mode 100644 index 000000000..17228e4ce --- /dev/null +++ b/examples/plugin-virtual-tour.html @@ -0,0 +1,169 @@ + + + + + + PhotoSphereViewer - virtual tour demo + + + + + + + + + +
+ + + + + + + + + + + diff --git a/examples/plugin-visible-range.html b/examples/plugin-visible-range.html new file mode 100644 index 000000000..de0562b24 --- /dev/null +++ b/examples/plugin-visible-range.html @@ -0,0 +1,67 @@ + + + + + + PhotoSphereViewer - visible range demo + + + + + +
+ + + + + + + + + diff --git a/examples/style.css b/examples/style.css new file mode 100644 index 000000000..4c53eeffe --- /dev/null +++ b/examples/style.css @@ -0,0 +1,18 @@ +html, +body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + font-family: sans-serif; +} + +#photosphere { + width: 100%; + height: 100%; +} + +.psv-button.custom-button { + font-size: 22px; + line-height: 20px; +} diff --git a/package.json b/package.json index 3cce01584..4f9d81cfc 100644 --- a/package.json +++ b/package.json @@ -1,101 +1,64 @@ { "name": "photo-sphere-viewer", - "version": "4.0.0-SNAPSHOT", - "description": "A JavaScript library to display Photo Sphere panoramas", - "homepage": "https://photo-sphere-viewer.js.org", - "main": "dist/photo-sphere-viewer.js", - "types": "dist/photo-sphere-viewer.d.ts", - "files": [ - "src/", - "dist/", - "types/" - ], - "authors": [ - { - "name": "Jérémy Heleine", - "email": "jeremy.heleine@gmail.com", - "homepage": "http://jeremyheleine.me" - }, - { - "name": "Damien \"Mistic\" Sorel", - "email": "contact@git.strangeplanet.fr", - "homepage": "https://www.strangeplanet.fr" - } - ], - "keywords": [ - "photosphere", - "panorama", - "threejs" - ], + "version": "0.0.0", + "private": true, "license": "MIT", - "repository": { - "type": "git", - "url": "git://github.com/mistic100/Photo-Sphere-Viewer.git" - }, - "dependencies": { - "three": "^0.147.0", - "uevent": "^2.1.1" + "workspaces": [ + "packages/*" + ], + "scripts": { + "format": "prettier -w .", + "build": "turbo run build", + "lint": "turbo run lint", + "test": "turbo run test", + "serve": "turbo run watch --parallel", + "doc:build": "yarn doc:vuepress && yarn doc:typedoc", + "doc:typedoc": "typedoc --out public/api", + "doc:vuepress": "vuepress build docs", + "doc:serve": "vuepress dev docs", + "ci:build": "turbo run lint build test --cache-dir=.turbo", + "ci:version": "set-versions --workspaces", + "ci:publish": "turbo run publish-dist --concurrency=1", + "watch": "node build/liveserver.mjs" }, "devDependencies": { - "@babel/core": "^7.15.0", - "@babel/eslint-parser": "^7.16.0", - "@babel/plugin-proposal-class-properties": "^7.14.5", - "@babel/plugin-proposal-optional-chaining": "^7.14.5", - "@babel/preset-env": "^7.15.0", - "@babel/register": "^7.15.3", - "@compodoc/live-server": "^1.2.3", - "@pixi/jsdoc-template": "^2.5.1", - "@rollup/plugin-babel": "^6.0.0", - "@rollup/plugin-json": "^5.0.0", - "@rollup/plugin-replace": "^5.0.0", - "@vuepress/plugin-active-header-links": "^1.8.2", - "@vuepress/plugin-back-to-top": "^1.8.2", - "@vuepress/plugin-google-analytics": "^1.8.2", - "autoprefixer": "^10.3.3", - "cpx2": "^4.1.2", - "date-fns": "^2.23.0", - "eslint": "^8.2.0", - "eslint-config-airbnb-base": "^15.0.0", - "eslint-plugin-import": "^2.25.2", - "jsdoc": "^3.6.7", - "marked": "^4.0.0", - "mocha": "^10.0.0", - "nosleep.js": "^0.12.0", - "npm-run-all": "^4.1.3", - "postcss": "^8.3.6", - "postcss-banner": "^4.0.1", + "@types/mocha": "^10.0.1", + "@types/three": "^0.146.0", + "@typescript-eslint/eslint-plugin": "^5.46.0", + "@typescript-eslint/parser": "^5.46.0", + "@vuepress/plugin-active-header-links": "^1.9.7", + "@vuepress/plugin-back-to-top": "^1.9.7", + "@vuepress/plugin-google-analytics": "^1.9.7", + "alive-server": "^1.2.9", + "codesandbox-import-utils": "^2.2.3", + "date-fns": "^2.29.3", + "esbuild-plugin-external-global": "^1.0.1", + "esbuild-sass-plugin": "^2.4.3", + "eslint": "^8.29.0", + "lodash": "^4.17.21", + "marked": "^4.2.3", + "mocha": "^10.1.0", + "postcss": "^8.4.19", + "prettier": "^2.8.0", "raw-loader": "^4.0.2", - "rollup": "^2.56.0", - "rollup-plugin-dts": "^4.0.0", - "rollup-plugin-local-resolve": "^1.0.7", - "rollup-plugin-postcss": "^4.0.2", - "rollup-plugin-string": "^3.0.0", - "sass": "^1.43.4", - "stylelint": "^14.9.0", - "stylelint-config-standard-scss": "^5.0.0", - "typescript": "^4.4.2", + "sass": "^1.56.1", + "scss-bundle": "^3.1.2", + "set-versions": "^1.0.3", + "sort-package-json": "^2.1.0", + "stylelint": "^14.16.0", + "stylelint-config-standard-scss": "^6.1.0", + "ts-node": "^10.9.1", + "tsup": "^6.5.0", + "turbo": "^1.6.3", + "typedoc": "^0.23.21", + "typedoc-plugin-extras": "^2.3.1", + "typedoc-plugin-resolve-crossmodule-references": "^0.3.2", + "typescript": "^4.9.3", "vue-material": "^1.0.0-beta-15", "vue-no-ssr": "^1.1.1", - "vue-slider-component": "^3.2.14", + "vue-slider-component": "^3.2.23", "vue-swatches": "^2.1.1", - "vuepress": "^1.8.2", - "yaml": "^2.1.1" - }, - "scripts": { - "compile": "rollup --config rollup.config.js", - "doc": "npm-run-all doc:*", - "doc:clean": "rm -rf public/*", - "doc:vuepress": "vuepress build docs", - "doc:jsdoc": "jsdoc --configure .jsdoc.json --destination public/api src", - "doc:assets": "cpx \"example/assets/*\" public/assets", - "test": "npm-run-all --parallel test:*", - "test:mocha": "mocha --require @babel/register --recursive \"src/**/*.spec.js\"", - "test:eslint": "eslint src --ignore-path .gitignore", - "test:sasslint": "stylelint \"src/**/*.scss\"", - "test:types": "cd tests && npm run test", - "start": "npm-run-all --parallel dev:**", - "start:doc": "vuepress dev docs", - "dev:serve": "node -e \"setTimeout(() => process.exit(0), 10000)\" && live-server --watch=dist,example --open=example", - "dev:watch": "npm run compile -- --watch" + "vuepress": "^1.9.7", + "yaml": "^2.1.3" } } diff --git a/packages/autorotate-plugin/.typedoc/README.md b/packages/autorotate-plugin/.typedoc/README.md new file mode 100644 index 000000000..672833ed4 --- /dev/null +++ b/packages/autorotate-plugin/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/autorotate-plugin](https://www.npmjs.com/package/@photo-sphere-viewer/autorotate-plugin) + +Documentation : https://photo-sphere-viewer.js.org/plugins/autorotate diff --git a/packages/autorotate-plugin/package.json b/packages/autorotate-plugin/package.json new file mode 100644 index 000000000..11fdab138 --- /dev/null +++ b/packages/autorotate-plugin/package.json @@ -0,0 +1,25 @@ +{ + "name": "@photo-sphere-viewer/autorotate-plugin", + "version": "0.0.0", + "description": "Photo sphere Viewer plugin to add an automatic rotation of the panorama.", + "homepage": "https://photo-sphere-viewer.js.org/plugins/autorotate", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.AutorotatePlugin" + }, + "typedoc": { + "displayName": "plugin: Autorotate", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/autorotate-plugin/src/AutorotateButton.ts b/packages/autorotate-plugin/src/AutorotateButton.ts new file mode 100644 index 000000000..d647cf00a --- /dev/null +++ b/packages/autorotate-plugin/src/AutorotateButton.ts @@ -0,0 +1,50 @@ +import type { Navbar } from '@photo-sphere-viewer/core'; +import { AbstractButton } from '@photo-sphere-viewer/core'; +import type { AutorotatePlugin } from './AutorotatePlugin'; +import { AutorotateEvent } from './events'; +import iconActive from './icons/play-active.svg'; +import icon from './icons/play.svg'; + +export class AutorotateButton extends AbstractButton { + static override readonly id = 'autorotate'; + + private readonly plugin: AutorotatePlugin; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-autorotate-button', + hoverScale: true, + collapsable: true, + tabbable: true, + icon: icon, + iconActive: iconActive, + }); + + this.plugin = this.viewer.getPlugin('autorotate'); + + this.plugin?.addEventListener(AutorotateEvent.type, this); + } + + override destroy() { + this.plugin?.removeEventListener(AutorotateEvent.type, this); + + super.destroy(); + } + + override isSupported() { + return !!this.plugin; + } + + handleEvent(e: Event) { + if (e instanceof AutorotateEvent) { + this.toggleActive(e.autorotateEnabled); + } + } + + onClick() { + if (this.plugin.isEnabled()) { + this.plugin.config.autostartOnIdle = false; + } + this.plugin.toggle(); + } +} diff --git a/packages/autorotate-plugin/src/AutorotatePlugin.ts b/packages/autorotate-plugin/src/AutorotatePlugin.ts new file mode 100644 index 000000000..0b982b13f --- /dev/null +++ b/packages/autorotate-plugin/src/AutorotatePlugin.ts @@ -0,0 +1,547 @@ +import type { ExtendedPosition, Position, Tooltip, Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin, CONSTANTS, events, PSVError, utils } from '@photo-sphere-viewer/core'; +import type { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin'; +import type { VideoPlugin } from '@photo-sphere-viewer/video-plugin'; +import { MathUtils, SplineCurve, Vector2 } from 'three'; +import { AutorotateEvent, AutorotatePluginEvents } from './events'; +import { AutorotateKeypoint, AutorotatePluginConfig } from './model'; +// import { debugCurve } from '@photo-sphere-viewer/shared'; + +type ParsedAutorotatePluginConfig = Omit< + AutorotatePluginConfig, + | 'autorotateSpeed' + | 'autorotatePitch' +> & { + autorotateSpeed?: number; + autorotatePitch?: number; +}; + +type AutorotateKeypointInternal = { + position: [number, number]; + markerId: string; + pause: number; + tooltip: { content: string; position?: string }; +}; + +const getConfig = utils.getConfigParser({ + autostartDelay: 2000, + autostartOnIdle: true, + autorotateSpeed: utils.parseSpeed('2rpm'), + autorotatePitch: null, + autorotateZoomLvl: null, + keypoints: null, + startFromClosest: true, +}, { + autostartOnIdle: (autostartOnIdle, { rawConfig }) => { + if (autostartOnIdle && utils.isNil(rawConfig.autostartDelay)) { + utils.logWarn('autostartOnIdle requires a non null autostartDelay'); + return false; + } + return autostartOnIdle; + }, + autorotateSpeed: (autorotateSpeed) => { + return utils.parseSpeed(autorotateSpeed); + }, + autorotatePitch: (autorotatePitch) => { + // autorotatePitch is between -PI/2 and PI/2 + if (!utils.isNil(autorotatePitch)) { + return utils.parseAngle(autorotatePitch, true); + } + return null; + }, + autorotateZoomLvl: (autorotateZoomLvl) => { + if (!utils.isNil(autorotateZoomLvl)) { + return MathUtils.clamp(autorotateZoomLvl, 0, 100); + } + return null; + }, +}); + +const NUM_STEPS = 16; + +function serializePt(position: Position): [number, number] { + return [position.yaw, position.pitch]; +} + +/** + * Adds an automatic rotation of the panorama + */ +export class AutorotatePlugin extends AbstractPlugin { + static override readonly id = 'autorotate'; + + readonly config: ParsedAutorotatePluginConfig; + + private readonly state = { + initialStart: true, + /** if the automatic rotation is enabled */ + enabled: false, + /** current index in keypoints */ + idx: -1, + /** curve between idx and idx + 1 */ + curve: [] as [number, number][], + /** start point of the current step */ + startStep: null as [number, number], + /** end point of the current step */ + endStep: null as [number, number], + /** start time of the current step */ + startTime: null as number, + /** expected duration of the step */ + stepDuration: null as number, + /** time remaining for the pause */ + remainingPause: null as number, + /** previous timestamp in render loop */ + lastTime: null as number, + /** currently displayed tooltip */ + tooltip: null as Tooltip, + }; + + private keypoints: AutorotateKeypointInternal[]; + + private video?: VideoPlugin; + private markers?: MarkersPlugin; + + constructor(viewer: Viewer, config: AutorotatePluginConfig) { + super(viewer); + + this.config = getConfig(config); + + this.state.initialStart = !utils.isNil(this.config.autostartDelay); + } + + /** + * @internal + */ + override init() { + super.init(); + + this.video = this.viewer.getPlugin('video'); + this.markers = this.viewer.getPlugin('markers'); + + if (this.config.keypoints) { + this.setKeypoints(this.config.keypoints); + delete this.config.keypoints; + } + + this.viewer.addEventListener(events.StopAllEvent.type, this); + this.viewer.addEventListener(events.BeforeRenderEvent.type, this); + + if (!this.video) { + this.viewer.addEventListener(events.KeypressEvent.type, this); + } + } + + /** + * @internal + */ + override destroy() { + this.viewer.removeEventListener(events.StopAllEvent.type, this); + this.viewer.removeEventListener(events.BeforeRenderEvent.type, this); + this.viewer.removeEventListener(events.KeypressEvent.type, this); + + delete this.video; + delete this.markers; + delete this.keypoints; + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + switch (e.type) { + case events.StopAllEvent.type: + this.stop(); + break; + + case events.BeforeRenderEvent.type: { + this.__beforeRender((e as events.BeforeRenderEvent).timestamp); + break; + } + + case events.KeypressEvent.type: + if ((e as events.KeypressEvent).key === CONSTANTS.KEY_CODES.Space) { + this.toggle(); + e.preventDefault(); + } + break; + } + } + + /** + * Changes the keypoints + * @throws {@link PSVError} if the configuration is invalid + */ + setKeypoints(keypoints: AutorotateKeypoint[]) { + if (!keypoints) { + this.keypoints = null; + + } else { + if (keypoints.length < 2) { + throw new PSVError('At least two points are required'); + } + + this.keypoints = keypoints.map((pt, i) => { + const keypoint: AutorotateKeypointInternal = { + position: null, + markerId: null, + pause: 0, + tooltip: null, + }; + + let position: ExtendedPosition; + + if (typeof pt === 'string') { + keypoint.markerId = pt; + } else if (utils.isExtendedPosition(pt)) { + position = pt; + } else { + keypoint.markerId = pt.markerId; + keypoint.pause = pt.pause; + position = pt.position; + + if (pt.tooltip && typeof pt.tooltip === 'object') { + keypoint.tooltip = pt.tooltip; + } else if (typeof pt.tooltip === 'string') { + keypoint.tooltip = { content: pt.tooltip }; + } + } + + if (keypoint.markerId) { + if (!this.markers) { + throw new PSVError(`Keypoint #${i} references a marker but the markers plugin is not loaded`); + } + const marker = this.markers.getMarker(keypoint.markerId); + keypoint.position = serializePt(marker.state.position); + } else if (position) { + keypoint.position = serializePt(this.viewer.dataHelper.cleanPosition(position)); + } else { + throw new PSVError(`Keypoint #${i} is missing marker or position`); + } + + return keypoint; + }); + } + + if (this.isEnabled()) { + this.stop(); + this.start(); + } + } + + /** + * Checks if the automatic rotation is enabled + */ + isEnabled(): boolean { + return this.state.enabled; + } + + /** + * Starts the automatic rotation + */ + start() { + if (this.isEnabled()) { + return; + } + + this.viewer.stopAll(); + + if (!this.keypoints) { + this.__animate(); + + } else if (this.config.startFromClosest) { + this.__shiftKeypoints(); + } + + this.state.initialStart = false; + this.state.enabled = true; + + this.dispatchEvent(new AutorotateEvent(true)); + } + + /** + * Stops the automatic rotation + */ + stop() { + if (!this.isEnabled()) { + return; + } + + this.__reset(); + this.__hideTooltip(); + + this.viewer.dynamics.position.stop(); + this.viewer.dynamics.zoom.stop(); + + this.state.enabled = false; + + this.dispatchEvent(new AutorotateEvent(false)); + } + + /** + * Starts or stops the automatic rotation + */ + toggle() { + if (this.isEnabled()) { + this.stop(); + } else { + this.start(); + } + } + + /** + * @internal + */ + reverse() { + if (this.isEnabled() && !this.keypoints) { + this.config.autorotateSpeed = -this.config.autorotateSpeed; + this.__animate(); + } + } + + /** + * Launches the standard animation + */ + private __animate() { + this.viewer.dynamics.position.roll( + { + yaw: this.config.autorotateSpeed < 0, + }, + Math.abs(this.config.autorotateSpeed / this.viewer.config.moveSpeed) + ); + + this.viewer.dynamics.position.goto( + { + pitch: this.config.autorotatePitch ?? this.viewer.config.defaultPitch, + }, + Math.abs(this.config.autorotateSpeed / this.viewer.config.moveSpeed) + ); + + if (!utils.isNil(this.config.autorotateZoomLvl)) { + this.viewer.dynamics.zoom.goto(this.config.autorotateZoomLvl); + } + } + + /** + * Resets all the curve variables + */ + private __reset() { + this.state.idx = -1; + this.state.curve = []; + this.state.startStep = null; + this.state.endStep = null; + this.state.startTime = null; + this.state.stepDuration = null; + this.state.remainingPause = null; + this.state.lastTime = null; + this.state.tooltip = null; + } + + /** + * Automatically starts if the delay is reached + * Performs keypoints animation + */ + private __beforeRender(timestamp: number) { + if ( + (this.state.initialStart || this.config.autostartOnIdle) + && this.viewer.state.idleTime > 0 + && timestamp - this.viewer.state.idleTime > this.config.autostartDelay + ) { + this.start(); + } + + if (this.isEnabled() && this.keypoints) { + // initialisation + if (!this.state.startTime) { + this.state.endStep = serializePt(this.viewer.getPosition()); + this.__nextStep(); + + this.state.startTime = timestamp; + this.state.lastTime = timestamp; + } + + this.__nextFrame(timestamp); + } + } + + private __shiftKeypoints() { + const currentPosition = serializePt(this.viewer.getPosition()); + const index = this.__findMinIndex(this.keypoints, (keypoint) => { + return utils.greatArcDistance(keypoint.position, currentPosition); + }); + + this.keypoints.push(...this.keypoints.splice(0, index)); + } + + private __incrementIdx() { + this.state.idx++; + if (this.state.idx === this.keypoints.length) { + this.state.idx = 0; + } + } + + private __showTooltip() { + const keypoint = this.keypoints[this.state.idx]; + + if (keypoint.tooltip) { + const position = this.viewer.dataHelper.vector3ToViewerCoords(this.viewer.state.direction); + + this.state.tooltip = this.viewer.createTooltip({ + content: keypoint.tooltip.content, + position: keypoint.tooltip.position, + top: position.y, + left: position.x, + }); + } else if (keypoint.markerId) { + const marker = this.markers.getMarker(keypoint.markerId); + marker.showTooltip(); + this.state.tooltip = marker.tooltip; + } + } + + private __hideTooltip() { + if (this.state.tooltip) { + const keypoint = this.keypoints[this.state.idx]; + + if (keypoint.tooltip) { + this.state.tooltip.hide(); + } else if (keypoint.markerId) { + const marker = this.markers.getMarker(keypoint.markerId); + marker.hideTooltip(); + } + + this.state.tooltip = null; + } + } + + private __nextPoint() { + // get the 4 points necessary to compute the current movement + // the two points of the current segments and one point before and after + const workPoints = []; + if (this.state.idx === -1) { + const currentPosition = serializePt(this.viewer.getPosition()); + // prettier-ignore + workPoints.push( + currentPosition, + currentPosition, + this.keypoints[0].position, + this.keypoints[1].position + ); + } else { + for (let i = -1; i < 3; i++) { + const keypoint = + this.state.idx + i < 0 + ? this.keypoints[this.keypoints.length - 1] + : this.keypoints[(this.state.idx + i) % this.keypoints.length]; + workPoints.push(keypoint.position); + } + } + + // apply offsets to avoid crossing the origin + const workVectors = [new Vector2(workPoints[0][0], workPoints[0][1])]; + + let k = 0; + for (let i = 1; i <= 3; i++) { + const d = workPoints[i - 1][0] - workPoints[i][0]; + if (d > Math.PI) { + // crossed the origin left to right + k += 1; + } else if (d < -Math.PI) { + // crossed the origin right to left + k -= 1; + } + if (k !== 0 && i === 1) { + // do not modify first point, apply the reverse offset the the previous point instead + workVectors[0].x -= k * 2 * Math.PI; + k = 0; + } + workVectors.push(new Vector2(workPoints[i][0] + k * 2 * Math.PI, workPoints[i][1])); + } + + const curve: [number, number][] = new SplineCurve(workVectors).getPoints(NUM_STEPS * 3).map((p) => [p.x, p.y]); + + // debugCurve(this.markers, curve, NUM_STEPS); + + // only keep the curve for the current movement + this.state.curve = curve.slice(NUM_STEPS + 1, NUM_STEPS * 2 + 1); + + if (this.state.idx !== -1) { + this.state.remainingPause = this.keypoints[this.state.idx].pause; + + if (this.state.remainingPause) { + this.__showTooltip(); + } else { + this.__incrementIdx(); + } + } else { + this.__incrementIdx(); + } + } + + private __nextStep() { + if (this.state.curve.length === 0) { + this.__nextPoint(); + + // reset transformation made to the previous point + this.state.endStep[0] = utils.parseAngle(this.state.endStep[0]); + } + + // target next point + this.state.startStep = this.state.endStep; + this.state.endStep = this.state.curve.shift(); + + // compute duration from distance and speed + const distance = utils.greatArcDistance(this.state.startStep, this.state.endStep); + this.state.stepDuration = (distance * 1000) / Math.abs(this.config.autorotateSpeed); + + if (distance === 0) { + // edge case + this.__nextStep(); + } + } + + private __nextFrame(timestamp: number) { + const ellapsed = timestamp - this.state.lastTime; + this.state.lastTime = timestamp; + + // currently paused + if (this.state.remainingPause) { + this.state.remainingPause = Math.max(0, this.state.remainingPause - ellapsed); + if (this.state.remainingPause > 0) { + return; + } else { + this.__hideTooltip(); + this.__incrementIdx(); + this.state.startTime = timestamp; + } + } + + let progress = (timestamp - this.state.startTime) / this.state.stepDuration; + if (progress >= 1) { + this.__nextStep(); + progress = 0; + this.state.startTime = timestamp; + } + + this.viewer.rotate({ + yaw: this.state.startStep[0] + (this.state.endStep[0] - this.state.startStep[0]) * progress, + pitch: this.state.startStep[1] + (this.state.endStep[1] - this.state.startStep[1]) * progress, + }); + } + + private __findMinIndex(array: T[], mapper: (item: T) => number) { + let idx = 0; + let current = Number.MAX_VALUE; + + array.forEach((item, i) => { + const value = mapper(item); + if (value < current) { + current = value; + idx = i; + } + }); + + return idx; + } + +} diff --git a/packages/autorotate-plugin/src/events.ts b/packages/autorotate-plugin/src/events.ts new file mode 100644 index 000000000..6df2894b9 --- /dev/null +++ b/packages/autorotate-plugin/src/events.ts @@ -0,0 +1,16 @@ +import { TypedEvent } from '@photo-sphere-viewer/core'; +import type { AutorotatePlugin } from './AutorotatePlugin'; + +/** + * @event Triggered when the automatic rotation is enabled/disabled + */ +export class AutorotateEvent extends TypedEvent { + static override readonly type = 'autorotate'; + + /** @internal */ + constructor(public readonly autorotateEnabled: boolean) { + super(AutorotateEvent.type); + } +} + +export type AutorotatePluginEvents = AutorotateEvent; diff --git a/packages/autorotate-plugin/src/icons/play-active.svg b/packages/autorotate-plugin/src/icons/play-active.svg new file mode 100644 index 000000000..6b73b5c03 --- /dev/null +++ b/packages/autorotate-plugin/src/icons/play-active.svg @@ -0,0 +1 @@ + diff --git a/packages/autorotate-plugin/src/icons/play.svg b/packages/autorotate-plugin/src/icons/play.svg new file mode 100644 index 000000000..776aa278f --- /dev/null +++ b/packages/autorotate-plugin/src/icons/play.svg @@ -0,0 +1 @@ + diff --git a/packages/autorotate-plugin/src/index.ts b/packages/autorotate-plugin/src/index.ts new file mode 100644 index 000000000..130cda031 --- /dev/null +++ b/packages/autorotate-plugin/src/index.ts @@ -0,0 +1,10 @@ +import { DEFAULTS, registerButton } from '@photo-sphere-viewer/core'; +import { AutorotateButton } from './AutorotateButton'; +import * as events from './events'; + +registerButton(AutorotateButton, 'start'); +DEFAULTS.lang[AutorotateButton.id] = 'Automatic rotation'; + +export { AutorotatePlugin } from './AutorotatePlugin'; +export * from './model'; +export { events }; diff --git a/packages/autorotate-plugin/src/model.ts b/packages/autorotate-plugin/src/model.ts new file mode 100644 index 000000000..e07eb67bb --- /dev/null +++ b/packages/autorotate-plugin/src/model.ts @@ -0,0 +1,60 @@ +import { ExtendedPosition } from '@photo-sphere-viewer/core'; + +/** + * Definition of keypoints for automatic rotation, can be a position object, a marker id or an configuration object + */ +export type AutorotateKeypoint = + | ExtendedPosition + | string + | { + position?: ExtendedPosition; + /** + * use the position and tooltip of a marker + */ + markerId?: string; + /** + * pause the animation when reaching this point, will display the tooltip if available + */ + pause?: number; + /** + * optional tooltip + */ + tooltip?: string | { content: string; position?: string }; + }; + +export type AutorotatePluginConfig = { + /** + * Delay after which the automatic rotation will begin, in milliseconds + * @default 2000 + */ + autostartDelay?: number; + /** + * Restarts the automatic rotation if the user is idle for `autostartDelay`. + * @default true + */ + autostartOnIdle?: boolean; + /** + * Speed of the automatic rotation. Can be a negative value to reverse the rotation. + * @default '2rpm' + */ + autorotateSpeed?: string | number; + /** + * Vertical angle at which the automatic rotation is performed. + * @default viewer `defaultPitch` + */ + autorotatePitch?: number | string; + /** + * Zoom level at which the automatic rotation is performed. + * @default current zoom level + */ + autorotateZoomLvl?: number; + /** + * List of positions to visit + */ + keypoints?: AutorotateKeypoint[]; + /** + * Start from the closest keypoint instead of the first keypoint + * @default true + */ + startFromClosest?: boolean; +}; diff --git a/packages/autorotate-plugin/tsconfig.json b/packages/autorotate-plugin/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/autorotate-plugin/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/autorotate-plugin/tsup.config.js b/packages/autorotate-plugin/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/autorotate-plugin/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/compass-plugin/.typedoc/README.md b/packages/compass-plugin/.typedoc/README.md new file mode 100644 index 000000000..b1248dff2 --- /dev/null +++ b/packages/compass-plugin/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/compass-plugin](https://www.npmjs.com/package/@photo-sphere-viewer/compass-plugin) + +Documentation : https://photo-sphere-viewer.js.org/plugins/compass diff --git a/packages/compass-plugin/package.json b/packages/compass-plugin/package.json new file mode 100644 index 000000000..ff0c4081d --- /dev/null +++ b/packages/compass-plugin/package.json @@ -0,0 +1,29 @@ +{ + "name": "@photo-sphere-viewer/compass-plugin", + "version": "0.0.0", + "description": "Photo sphere Viewer plugin to add a compass to represent which portion of the sphere is currently visible.", + "homepage": "https://photo-sphere-viewer.js.org/plugins/compass", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "optionalDependencies": { + "@photo-sphere-viewer/markers-plugin": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix && stylelint \"src/**/*.scss\" --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.CompassPlugin", + "style": true + }, + "typedoc": { + "displayName": "plugin: Compass", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/compass-plugin/src/CompassPlugin.ts b/packages/compass-plugin/src/CompassPlugin.ts new file mode 100644 index 000000000..ab010298c --- /dev/null +++ b/packages/compass-plugin/src/CompassPlugin.ts @@ -0,0 +1,332 @@ +import type { Position, Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin, events, SYSTEM, utils } from '@photo-sphere-viewer/core'; +import type { events as markersEvents, Marker, MarkersPlugin } from '@photo-sphere-viewer/markers-plugin'; +import { MathUtils } from 'three'; +import compass from './compass.svg'; +import { CompassHotspot, CompassPluginConfig, ParsedCompassPluginConfig } from './model'; + +const getConfig = utils.getConfigParser( + { + size: '120px', + position: ['top', 'left'], + backgroundSvg: compass, + coneColor: 'rgba(255, 255, 255, 0.5)', + navigation: true, + navigationColor: 'rgba(255, 0, 0, 0.2)', + hotspots: [], + hotspotColor: 'rgba(0, 0, 0, 0.5)', + }, + { + position: (position, { defValue }) => { + return utils.cleanCssPosition(position, { allowCenter: true, cssOrder: true }) || defValue; + }, + } +); + +const HOTSPOT_SIZE_RATIO = 1 / 40; + +/** + * Adds a compass on the viewer + */ +export class CompassPlugin extends AbstractPlugin { + static override readonly id = 'compass'; + + readonly config: ParsedCompassPluginConfig; + + private readonly state = { + visible: true, + mouse: null as { clientX: number; clientY: number }, + mousedown: false, + markers: [] as Marker[], + }; + + private markers?: MarkersPlugin; + private readonly container: HTMLElement; + private readonly canvas: HTMLCanvasElement; + + constructor(viewer: Viewer, config: CompassPluginConfig) { + super(viewer); + + this.config = getConfig(config); + + this.container = document.createElement('div'); + this.container.className = `psv-compass psv-compass--${this.config.position.join('-')}`; + this.container.innerHTML = this.config.backgroundSvg; + + this.container.style.width = this.config.size; + this.container.style.height = this.config.size; + if (this.config.position[0] === 'center') { + this.container.style.marginTop = `calc(-${this.config.size} / 2)`; + } + if (this.config.position[1] === 'center') { + this.container.style.marginLeft = `calc(-${this.config.size} / 2)`; + } + + this.canvas = document.createElement('canvas'); + + this.container.appendChild(this.canvas); + + if (this.config.navigation) { + this.container.addEventListener('mouseenter', this); + this.container.addEventListener('mouseleave', this); + this.container.addEventListener('mousemove', this); + this.container.addEventListener('mousedown', this); + this.container.addEventListener('mouseup', this); + this.container.addEventListener('touchstart', this); + this.container.addEventListener('touchmove', this); + this.container.addEventListener('touchend', this); + } + } + + /** + * @internal + */ + override init() { + super.init(); + + this.markers = this.viewer.getPlugin('markers'); + + this.viewer.container.appendChild(this.container); + + this.canvas.width = this.container.clientWidth * SYSTEM.pixelRatio; + this.canvas.height = this.container.clientWidth * SYSTEM.pixelRatio; + + this.viewer.addEventListener(events.RenderEvent.type, this); + + if (this.markers) { + this.markers.addEventListener('set-markers', this); + } + } + + /** + * @internal + */ + override destroy() { + this.viewer.removeEventListener(events.RenderEvent.type, this); + + if (this.markers) { + this.markers.removeEventListener('set-markers', this); + } + + this.viewer.container.removeChild(this.container); + + delete this.markers; + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + switch (e.type) { + case events.RenderEvent.type: + this.__update(); + break; + case 'set-markers': + this.state.markers = (e as markersEvents.SetMarkersEvent).markers.filter((m) => m.data?.['compass']); + this.__update(); + break; + case 'mouseenter': + case 'mousemove': + case 'touchmove': + this.state.mouse = (e as TouchEvent).changedTouches?.[0] || (e as MouseEvent); + if (this.state.mousedown) { + this.__click(); + } else { + this.__update(); + } + e.stopPropagation(); + e.preventDefault(); + break; + case 'mousedown': + case 'touchstart': + this.state.mousedown = true; + e.stopPropagation(); + e.preventDefault(); + break; + case 'mouseup': + case 'touchend': + this.state.mouse = (e as TouchEvent).changedTouches?.[0] || (e as MouseEvent); + this.state.mousedown = false; + this.__click(); + if ((e as TouchEvent).changedTouches) { + this.state.mouse = null; + this.__update(); + } + e.stopPropagation(); + e.preventDefault(); + break; + case 'mouseleave': + this.state.mouse = null; + this.state.mousedown = false; + this.__update(); + break; + default: + break; + } + } + + /** + * Hides the compass + */ + hide() { + this.container.style.display = 'none'; + this.state.visible = false; + } + + /** + * Shows the compass + */ + show() { + this.container.style.display = ''; + this.state.visible = true; + } + + /** + * Changes the hotspots on the compass + */ + setHotspots(hotspots: CompassHotspot[]) { + this.config.hotspots = hotspots; + this.__update(); + } + + /** + * Removes all hotspots + */ + clearHotspots() { + this.setHotspots(null); + } + + /** + * Updates the compass for current zoom and position + */ + private __update() { + const context = this.canvas.getContext('2d'); + context.clearRect(0, 0, this.canvas.width, this.canvas.height); + + const yaw = this.viewer.getPosition().yaw; + const fov = MathUtils.degToRad(this.viewer.state.hFov); + + this.__drawCone(context, this.config.coneColor, yaw, fov); + + const mouseAngle = this.__getMouseAngle(); + if (mouseAngle !== null) { + this.__drawCone(context, this.config.navigationColor, mouseAngle, fov); + } + + this.state.markers.forEach((marker) => { + this.__drawMarker(context, marker); + }); + this.config.hotspots?.forEach((spot) => { + if ('yaw' in spot && !('pitch' in spot)) { + (spot as Position).pitch = 0; + } + const pos = this.viewer.dataHelper.cleanPosition(spot); + this.__drawPoint(context, spot.color || this.config.hotspotColor, pos.yaw, pos.pitch); + }); + } + + /** + * Rotates the viewer depending on the position of the mouse on the compass + */ + private __click() { + const mouseAngle = this.__getMouseAngle(); + + if (mouseAngle !== null) { + this.viewer.rotate({ + yaw: mouseAngle, + pitch: 0, // TODO marker or hotspot vertical angle + }); + } + } + + /** + * Draw a cone + */ + private __drawCone(context: CanvasRenderingContext2D, color: string, yaw: number, fov: number) { + const a1 = yaw - Math.PI / 2 - fov / 2; + const a2 = a1 + fov; + const c = this.canvas.width / 2; + + context.beginPath(); + context.moveTo(c, c); + context.lineTo(c + Math.cos(a1) * c, c + Math.sin(a1) * c); + context.arc(c, c, c, a1, a2, false); + context.lineTo(c, c); + context.fillStyle = color; + context.fill(); + } + + /** + * Draw a Marker + */ + private __drawMarker(context: CanvasRenderingContext2D, marker: Marker) { + let color = this.config.hotspotColor; + if (typeof marker.data['compass'] === 'string') { + color = marker.data['compass']; + } + + if (marker.isPoly()) { + context.beginPath(); + (marker.definition as [number, number][]).forEach(([yaw, pitch], i) => { + const a = yaw - Math.PI / 2; + const d = (pitch + Math.PI / 2) / Math.PI; + const c = this.canvas.width / 2; + + context[i === 0 ? 'moveTo' : 'lineTo'](c + Math.cos(a) * c * d, c + Math.sin(a) * c * d); + }); + if (marker.isPolygon()) { + context.fillStyle = color; + context.fill(); + } else { + context.strokeStyle = color; + context.lineWidth = Math.max(1, (this.canvas.width * HOTSPOT_SIZE_RATIO) / 2); + context.stroke(); + } + } else { + const pos = marker.state.position; + this.__drawPoint(context, color, pos.yaw, pos.pitch); + } + } + + /** + * Draw a point + */ + private __drawPoint(context: CanvasRenderingContext2D, color: string, yaw: number, pitch: number) { + const a = yaw - Math.PI / 2; + const d = (pitch + Math.PI / 2) / Math.PI; + const c = this.canvas.width / 2; + const r = Math.max(2, this.canvas.width * HOTSPOT_SIZE_RATIO); + + context.beginPath(); + // prettier-ignore + context.ellipse( + c + Math.cos(a) * c * d, c + Math.sin(a) * c * d, + r, r, + 0, 0, + Math.PI * 2 + ); + context.fillStyle = color; + context.fill(); + } + + /** + * Gets the horizontal angle corresponding to the mouse position on the compass + */ + private __getMouseAngle(): number | null { + if (!this.state.mouse) { + return null; + } + + const boundingRect = this.container.getBoundingClientRect(); + const mouseX = this.state.mouse.clientX - boundingRect.left - boundingRect.width / 2; + const mouseY = this.state.mouse.clientY - boundingRect.top - boundingRect.width / 2; + + if (Math.sqrt(mouseX * mouseX + mouseY * mouseY) > boundingRect.width / 2) { + return null; + } + + return Math.atan2(mouseY, mouseX) + Math.PI / 2; + } +} diff --git a/src/plugins/compass/compass.svg b/packages/compass-plugin/src/compass.svg similarity index 100% rename from src/plugins/compass/compass.svg rename to packages/compass-plugin/src/compass.svg diff --git a/packages/compass-plugin/src/index.ts b/packages/compass-plugin/src/index.ts new file mode 100644 index 000000000..a26f2cbc1 --- /dev/null +++ b/packages/compass-plugin/src/index.ts @@ -0,0 +1,5 @@ +export { CompassPlugin } from './CompassPlugin'; +export * from './model'; + +/** @internal */ +import './style.scss'; diff --git a/packages/compass-plugin/src/model.ts b/packages/compass-plugin/src/model.ts new file mode 100644 index 000000000..3a0f2def2 --- /dev/null +++ b/packages/compass-plugin/src/model.ts @@ -0,0 +1,60 @@ +import { ExtendedPosition } from '@photo-sphere-viewer/core'; + +export type CompassHotspot = ExtendedPosition & { + /** + * override the global "hotspotColor" + */ + color?: string; +}; + +export type CompassPluginConfig = { + /** + * size of the compass + * @default '120px' + */ + size?: string; + + /** + * position of the compass + * @default 'top left' + */ + position?: string | [string, string]; + + /** + * SVG used as background of the compass + */ + backgroundSvg?: string; + + /** + * color of the cone of the compass + * @default 'rgba(255, 255, 255, 0.5)' + */ + coneColor?: string; + + /** + * allows to click on the compass to rotate the viewer + * @default true + */ + navigation?: boolean; + + /** + * color of the navigation cone + * @default 'rgba(255, 0, 0, 0.2)' + */ + navigationColor?: string; + + /** + * small dots visible on the compass (will contain every marker with the "compass" data) + */ + hotspots?: CompassHotspot[]; + + /** + * default color of hotspots + * @default 'rgba(0, 0, 0, 0.5)' + */ + hotspotColor?: string; +}; + +export type ParsedCompassPluginConfig = Omit & { + position: [string, string]; +}; diff --git a/packages/compass-plugin/src/style.scss b/packages/compass-plugin/src/style.scss new file mode 100644 index 000000000..3e9a46d5f --- /dev/null +++ b/packages/compass-plugin/src/style.scss @@ -0,0 +1,58 @@ +@import '../../shared/src/vars'; + +$psv-compass-margin: 10px !default; + +.psv-compass { + position: absolute; + margin: $psv-compass-margin; + z-index: $psv-compass-zindex; + + @at-root .psv--has-navbar & { + margin-bottom: calc(#{$psv-navbar-height} + #{$psv-compass-margin}); + } + + canvas, + svg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + &--top-left, + &--top-center, + &--top-right { + top: 0; + } + + &--center-left, + &--center-center, + &--center-right { + top: 50%; + } + + &--bottom-left, + &--bottom-center, + &--bottom-right { + bottom: 0; + } + + &--top-left, + &--center-left, + &--bottom-left { + left: 0; + } + + &--top-center, + &--center-center, + &--bottom-center { + left: 50%; + } + + &--top-right, + &--right-right, + &--bottom-right { + right: 0; + } +} diff --git a/packages/compass-plugin/tsconfig.json b/packages/compass-plugin/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/compass-plugin/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/compass-plugin/tsup.config.js b/packages/compass-plugin/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/compass-plugin/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/core/.typedoc/README.md b/packages/core/.typedoc/README.md new file mode 100644 index 000000000..fc004e7a6 --- /dev/null +++ b/packages/core/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/core](https://www.npmjs.com/package/@photo-sphere-viewer/core) + +Documentation : https://photo-sphere-viewer.js.org/guide/ diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 000000000..e18b14afa --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,27 @@ +{ + "name": "@photo-sphere-viewer/core", + "version": "0.0.0", + "description": "A JavaScript library to display Photo Sphere panoramas on any web page.", + "homepage": "https://photo-sphere-viewer.js.org", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "three": "^0.147.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix && stylelint \"src/**/*.scss\" --fix", + "test": "mocha -r ts-node/register \"src/**/*.spec.ts\"", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer", + "style": true + }, + "typedoc": { + "displayName": "Core", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/core/src/PSVError.ts b/packages/core/src/PSVError.ts new file mode 100644 index 000000000..9ee96d7a1 --- /dev/null +++ b/packages/core/src/PSVError.ts @@ -0,0 +1,7 @@ +export class PSVError extends Error { + constructor(message: string) { + super(message); + this.name = 'PSVError'; + (Error as any).captureStackTrace?.(this, PSVError); + } +} diff --git a/packages/core/src/Viewer.ts b/packages/core/src/Viewer.ts new file mode 100644 index 000000000..e7831e152 --- /dev/null +++ b/packages/core/src/Viewer.ts @@ -0,0 +1,792 @@ +import type { AbstractAdapter } from './adapters/AbstractAdapter'; +import type { AbstractComponent } from './components/AbstractComponent'; +import { Loader } from './components/Loader'; +import { Navbar } from './components/Navbar'; +import { Notification } from './components/Notification'; +import { Overlay } from './components/Overlay'; +import { Panel } from './components/Panel'; +import { Tooltip, TooltipConfig } from './components/Tooltip'; +import { CONFIG_PARSERS, DEFAULTS, getViewerConfig, READONLY_OPTIONS } from './data/config'; +import { ANIMATION_MIN_DURATION, DEFAULT_TRANSITION, IDS, VIEWER_DATA } from './data/constants'; +import { SYSTEM } from './data/system'; +import { + BeforeAnimateEvent, + BeforeRotateEvent, + ConfigChangedEvent, + PanoramaLoadedEvent, + ReadyEvent, + SizeUpdatedEvent, + StopAllEvent, + ViewerEvents, + ZoomUpdatedEvent, +} from './events'; +import errorIcon from './icons/error.svg'; +import { TypedEventTarget } from './lib/TypedEventTarget'; +import { + AnimateOptions, + CssSize, + DeprecatedViewerConfig, + ExtendedPosition, + PanoramaOptions, + ParsedViewerConfig, + Position, + PositionCompat, + Size, + UpdatableViewerConfig, + ViewerConfig, +} from './model'; +import type { AbstractPlugin } from './plugins/AbstractPlugin'; +import { pluginInterop } from './plugins/AbstractPlugin'; +import { PSVError } from './PSVError'; +import { DataHelper } from './services/DataHelper'; +import { EventsHandler } from './services/EventsHandler'; +import { Renderer } from './services/Renderer'; +import { TextureLoader } from './services/TextureLoader'; +import { ViewerDynamics } from './services/ViewerDynamics'; +import { ViewerState } from './services/ViewerState'; +import { + Animation, + getAbortError, + getAngle, + getElement, + getShortestArc, + isAbortError, + isExtendedPosition, + logWarn, + positionCompat, + throttle, + toggleClass, +} from './utils'; + +/** + * Photo Sphere Viewer controller + */ +export class Viewer extends TypedEventTarget { + readonly state: ViewerState; + readonly config: ParsedViewerConfig; + + readonly parent: HTMLElement; + readonly container: HTMLElement; + + /** @internal */ + readonly adapter: AbstractAdapter; + /** @internal */ + readonly plugins: Record> = {}; + /** @internal */ + readonly dynamics: ViewerDynamics; + + readonly renderer: Renderer; + readonly textureLoader: TextureLoader; + /** @internal */ + readonly eventsHandler: EventsHandler; + readonly dataHelper: DataHelper; + + readonly loader: Loader; + readonly navbar: Navbar; + readonly notification: Notification; + readonly overlay: Overlay; + readonly panel: Panel; + + /** @internal */ + readonly children: AbstractComponent[] = []; + + private readonly onResize = throttle(() => this.navbar.autoSize(), 500); + + /** @deprecated Use {@link createTooltip} instead */ + readonly tooltip = { + /** @deprecated */ + create: (config: TooltipConfig) => { + logWarn(`tooltip.create() is deprecated, use createTooltip() instead`); + return this.createTooltip(config); + }, + }; + + constructor(config: ViewerConfig) { + super(); + + SYSTEM.load(); + + this.state = new ViewerState(); + this.config = getViewerConfig(config); + + this.parent = getElement(config.container); + // @ts-ignore + this.parent[VIEWER_DATA] = this; + + this.container = document.createElement('div'); + this.container.classList.add('psv-container'); + this.parent.appendChild(this.container); + + // @ts-ignore + this.adapter = new this.config.adapter[0](this, this.config.adapter[1]); + + this.renderer = new Renderer(this); + this.textureLoader = new TextureLoader(this); + this.eventsHandler = new EventsHandler(this); + this.dataHelper = new DataHelper(this); + this.dynamics = new ViewerDynamics(this); + + this.loader = new Loader(this); + this.navbar = new Navbar(this); + this.panel = new Panel(this); + this.notification = new Notification(this); + this.overlay = new Overlay(this); + + // init + this.resize(this.config.size); + + toggleClass(this.container, 'psv--is-touch', SYSTEM.isTouchEnabled.initial); + SYSTEM.isTouchEnabled.promise.then((enabled) => toggleClass(this.container, 'psv--is-touch', enabled)); + + // init plugins + this.config.plugins.forEach(([plugin, opts]) => { + // @ts-ignore + this.plugins[plugin.id] = new plugin(this, opts); + }); + for (const plugin of Object.values(this.plugins)) { + plugin.init?.(); + } + + // init buttons + if (this.config.navbar) { + this.navbar.setButtons(this.config.navbar); + } + + // load panorama + if (this.config.panorama) { + this.setPanorama(this.config.panorama); + } else { + this.loader.show(); + } + } + + /** + * Destroys the viewer + */ + destroy() { + this.stopAll(); + this.stopKeyboardControl(); + this.exitFullscreen(); + + for (const [id, plugin] of Object.entries(this.plugins)) { + plugin.destroy(); + delete this.plugins[id]; + } + + this.children.slice().forEach((child) => child.destroy()); + this.children.length = 0; + + this.eventsHandler.destroy(); + this.renderer.destroy(); + this.textureLoader.destroy(); + this.dataHelper.destroy(); + this.adapter.destroy(); + this.dynamics.destroy(); + + this.parent.removeChild(this.container); + // @ts-ignore + delete this.parent[VIEWER_DATA]; + } + + private init() { + this.eventsHandler.init(); + this.renderer.init(); + + if (this.config.navbar) { + this.container.classList.add('psv--has-navbar'); + this.navbar.show(); + } + + this.resetIdleTimer(); + + this.state.ready = true; + + this.dispatchEvent(new ReadyEvent()); + } + + /** + * Restarts the idle timer + * @internal + */ + resetIdleTimer() { + this.state.idleTime = performance.now(); + } + + /** + * Stops the idle timer + * @internal + */ + disableIdleTimer() { + this.state.idleTime = -1; + } + + /** + * Returns the instance of a plugin if it exists + */ + getPlugin>(pluginId: string | typeof AbstractPlugin): T { + if (typeof pluginId === 'string') { + return this.plugins[pluginId] as T; + } else { + const pluginCtor = pluginInterop(pluginId); + return pluginCtor ? (this.plugins[pluginCtor.id] as T) : null; + } + } + + /** + * Returns the current position of the camera + */ + getPosition(): PositionCompat { + return positionCompat(this.dataHelper.cleanPosition(this.dynamics.position.current)); + } + + /** + * Returns the current zoom level + */ + getZoomLevel(): number { + return this.dynamics.zoom.current; + } + + /** + * Returns the current viewer size + */ + getSize(): Size { + return { ...this.state.size }; + } + + /** + * Checks if the viewer is in fullscreen + */ + isFullscreenEnabled(): boolean { + return document.fullscreenElement === this.container; + } + + /** + * Request a new render of the scene + */ + needsUpdate() { + this.state.needsUpdate = true; + } + + /** + * Resizes the scene if the viewer is resized + */ + autoSize() { + if ( + this.container.clientWidth !== this.state.size.width + || this.container.clientHeight !== this.state.size.height + ) { + this.state.size.width = Math.round(this.container.clientWidth); + this.state.size.height = Math.round(this.container.clientHeight); + this.state.aspect = this.state.size.width / this.state.size.height; + this.state.hFov = this.dataHelper.vFovToHFov(this.state.vFov); + + this.dispatchEvent(new SizeUpdatedEvent(this.getSize())); + this.onResize(); + } + } + + /** + * Loads a new panorama file + * @description Loads a new panorama file, optionally changing the camera position/zoom and activating the transition animation.
+ * If the "options" parameter is not defined, the camera will not move and the ongoing animation will continue.
+ * If another loading is already in progress it will be aborted. + * @returns promise resolved with false if the loading was aborted by another call + */ + setPanorama(path: any, options: PanoramaOptions = {}): Promise { + this.textureLoader.abortLoading(); + this.state.transitionAnimation?.cancel(); + + // apply default parameters on first load + if (!this.state.ready) { + ['sphereCorrection', 'panoData', 'overlay', 'overlayOpacity'].forEach((opt) => { + if (!(opt in options)) { + // @ts-ignore + options[opt] = this.config[opt]; + } + }); + } + + if (options.transition === undefined || options.transition === true) { + options.transition = DEFAULT_TRANSITION; + } + if (options.showLoader === undefined) { + options.showLoader = true; + } + if (options.caption === undefined) { + options.caption = this.config.caption; + } + if (options.description === undefined) { + options.description = this.config.description; + } + if (!options.panoData && typeof this.config.panoData === 'function') { + options.panoData = this.config.panoData; + } + + const positionProvided = isExtendedPosition(options); + const zoomProvided = 'zoom' in options; + + if (positionProvided || zoomProvided) { + this.stopAll(); + } + + this.hideError(); + this.resetIdleTimer(); + + this.config.panorama = path; + this.config.caption = options.caption; + this.config.description = options.description; + + const done = (err?: Error) => { + this.loader.hide(); + + this.state.loadingPromise = null; + + if (isAbortError(err)) { + return false; + } else if (err) { + this.navbar.setCaption(''); + this.showError(this.config.lang.loadError); + console.error(err); + throw err; + } else { + this.setOverlay(options.overlay, options.overlayOpacity); + this.navbar.setCaption(this.config.caption); + return true; + } + }; + + this.navbar.setCaption(`${this.config.loadingTxt || ''}`); + if (options.showLoader || !this.state.ready) { + this.loader.show(); + } + + const loadingPromise = this.adapter.loadTexture(this.config.panorama, options.panoData).then((textureData) => { + // check if another panorama was requested + if (textureData.panorama !== this.config.panorama) { + this.adapter.disposeTexture(textureData); + throw getAbortError(); + } + return textureData; + }); + + if (!options.transition || !this.state.ready || !this.adapter.supportsTransition(this.config.panorama)) { + this.state.loadingPromise = loadingPromise + .then((textureData) => { + this.renderer.show(); + this.renderer.setTexture(textureData); + this.renderer.setPanoramaPose(textureData.panoData); + this.renderer.setSphereCorrection(options.sphereCorrection); + + this.dispatchEvent(new PanoramaLoadedEvent(textureData)); + + if (zoomProvided) { + this.zoom(options.zoom); + } + if (positionProvided) { + this.rotate(options); + } + + if (!this.state.ready) { + this.init(); + } + }) + .then( + () => done(), + (err) => done(err) + ); + } else { + this.state.loadingPromise = loadingPromise + .then((textureData) => { + this.loader.hide(); + + this.state.transitionAnimation = this.renderer.transition(textureData, options); + return this.state.transitionAnimation; + }) + .then((completed) => { + this.state.transitionAnimation = null; + if (!completed) { + throw getAbortError(); + } + }) + .then( + () => done(), + (err) => done(err) + ); + } + + return this.state.loadingPromise; + } + + /** + * Loads a new overlay + */ + setOverlay(path: any, opacity = this.config.overlayOpacity): Promise { + const supportsOverlay = (this.adapter.constructor as typeof AbstractAdapter).supportsOverlay; + + if (!path) { + if (supportsOverlay) { + this.renderer.setOverlay(null, 0); + } + + return Promise.resolve(); + } else { + if (!supportsOverlay) { + return Promise.reject(new PSVError(`Current adapter does not supports overlay`)); + } + + return this.adapter + .loadTexture( + path, + (image) => { + const p = this.state.panoData; + const r = image.width / p.croppedWidth; + return { + fullWidth: r * p.fullWidth, + fullHeight: r * p.fullHeight, + croppedWidth: r * p.croppedWidth, + croppedHeight: r * p.croppedHeight, + croppedX: r * p.croppedX, + croppedY: r * p.croppedY, + }; + }, + false + ) + .then((textureData) => { + this.renderer.setOverlay(textureData, opacity); + }); + } + } + + /** + * Update options + * @throws {@link PSVError} if the configuration is invalid + */ + setOptions(options: Partial) { + const rawConfig: Omit = { + ...this.config, + ...options, + }; + + for (let [key, value] of Object.entries(options) as [keyof typeof rawConfig, any][]) { + if (!(key in DEFAULTS)) { + logWarn(`Unknown option ${key}`); + continue; + } + + if (key in READONLY_OPTIONS) { + logWarn((READONLY_OPTIONS as any)[key]); + continue; + } + + if (key in CONFIG_PARSERS) { + // @ts-ignore + value = CONFIG_PARSERS[key](value, { + rawConfig: rawConfig, + defValue: DEFAULTS[key], + } as any); + } + + // @ts-ignore + this.config[key] = value; + + switch (key) { + case 'caption': + this.navbar.setCaption(this.config.caption); + break; + + case 'size': + this.resize(this.config.size); + break; + + case 'sphereCorrection': + this.renderer.setSphereCorrection(this.config.sphereCorrection); + break; + + case 'navbar': + case 'lang': + this.navbar.setButtons(this.config.navbar); + break; + + case 'moveSpeed': + case 'zoomSpeed': + this.dynamics.updateSpeeds(); + break; + + case 'minFov': + case 'maxFov': + this.dynamics.zoom.setValue(this.dataHelper.fovToZoomLevel(this.state.vFov)); + this.dispatchEvent(new ZoomUpdatedEvent(this.getZoomLevel())); + break; + + default: + break; + } + } + + this.needsUpdate(); + + this.dispatchEvent(new ConfigChangedEvent(Object.keys(options) as any)); + } + + /** + * Update options + * @throws {@link PSVError} if the configuration is invalid + */ + setOption(option: T, value: UpdatableViewerConfig[T]) { + this.setOptions({ [option]: value }); + } + + /** + * Displays an error message over the viewer + */ + showError(message: string) { + this.overlay.show({ + id: IDS.ERROR, + image: errorIcon, + title: message, + dissmisable: false, + }); + } + + /** + * Hides the error message + */ + hideError() { + this.overlay.hide(IDS.ERROR); + } + + /** + * Rotates the view to specific position + */ + rotate(position: ExtendedPosition) { + const e = new BeforeRotateEvent(this.dataHelper.cleanPosition(position)); + this.dispatchEvent(e); + + if (e.defaultPrevented) { + return; + } + + this.dynamics.position.setValue(e.position); + } + + /** + * Zooms to a specific level between `maxFov` and `minFov` + */ + zoom(level: number) { + this.dynamics.zoom.setValue(level); + } + + /** + * Increases the zoom level + */ + zoomIn(step = 1) { + this.dynamics.zoom.step(step); + } + + /** + * Decreases the zoom level + */ + zoomOut(step = 1) { + this.dynamics.zoom.step(-step); + } + + /** + * Rotates and zooms the view with a smooth animation + */ + animate(options: AnimateOptions): Animation { + const positionProvided = isExtendedPosition(options); + const zoomProvided = options.zoom !== undefined; + + const e = new BeforeAnimateEvent( + positionProvided ? this.dataHelper.cleanPosition(options) : undefined, + options.zoom + ); + this.dispatchEvent(e); + + if (e.defaultPrevented) { + return; + } + + const cleanPosition = e.position as Position; + const cleanZoom = e.zoomLevel; + + this.stopAll(); + + const animProperties: { + yaw?: { start: number; end: number }; + pitch?: { start: number; end: number }; + zoom?: { start: number; end: number }; + } = {}; + let duration; + + // clean/filter position and compute duration + if (positionProvided) { + const currentPosition = this.getPosition(); + + // horizontal offset for shortest arc + const tOffset = getShortestArc(currentPosition.yaw, cleanPosition.yaw); + + animProperties.yaw = { start: currentPosition.yaw, end: currentPosition.yaw + tOffset }; + animProperties.pitch = { start: currentPosition.pitch, end: cleanPosition.pitch }; + + duration = this.dataHelper.speedToDuration(options.speed, getAngle(currentPosition, cleanPosition)); + } + + // clean/filter zoom and compute duration + if (zoomProvided) { + const dZoom = Math.abs(cleanZoom - this.getZoomLevel()); + + animProperties.zoom = { start: this.getZoomLevel(), end: cleanZoom }; + + if (!duration) { + // if animating zoom only and a speed is given, use an arbitrary PI/4 to compute the duration + duration = this.dataHelper.speedToDuration(options.speed, ((Math.PI / 4) * dZoom) / 100); + } + } + + // if no animation needed + if (!duration) { + if (positionProvided) { + this.rotate(cleanPosition); + } + if (zoomProvided) { + this.zoom(cleanZoom); + } + + return new Animation(null); + } + + this.state.animation = new Animation({ + properties: animProperties, + duration: Math.max(ANIMATION_MIN_DURATION, duration), + easing: 'inOutSine', + onTick: (properties) => { + if (positionProvided) { + this.dynamics.position.setValue({ + yaw: properties.yaw, + pitch: properties.pitch, + }); + } + if (zoomProvided) { + this.dynamics.zoom.setValue(properties.zoom); + } + }, + }); + + this.state.animation.then(() => { + this.state.animation = null; + this.resetIdleTimer(); + }); + + return this.state.animation; + } + + /** + * Stops the ongoing animation + * @description The return value is a Promise because the is no guaranty the animation can be stopped synchronously. + */ + stopAnimation(): PromiseLike { + if (this.state.animation) { + this.state.animation.cancel(); + return this.state.animation; + } else { + return Promise.resolve(); + } + } + + /** + * Resizes the viewer + */ + resize(size: CssSize) { + const s = size as any; + (['width', 'height'] as ('width' | 'height')[]).forEach((dim) => { + if (size && s[dim]) { + if (/^[0-9.]+$/.test(s[dim])) { + s[dim] += 'px'; + } + this.parent.style[dim] = s[dim]; + } + }); + + this.autoSize(); + } + + /** + * Enters the fullscreen mode + */ + enterFullscreen() { + if (!this.isFullscreenEnabled()) { + this.container.requestFullscreen(); + } + } + + /** + * Exits the fullscreen mode + */ + exitFullscreen() { + if (this.isFullscreenEnabled()) { + document.exitFullscreen(); + } + } + + /** + * Enters or exits the fullscreen mode + */ + toggleFullscreen() { + if (!this.isFullscreenEnabled()) { + this.enterFullscreen(); + } else { + this.exitFullscreen(); + } + } + + /** + * Enables the keyboard controls (done automatically when entering fullscreen) + */ + startKeyboardControl() { + this.state.keyboardEnabled = true; + } + + /** + * Disables the keyboard controls (done automatically when exiting fullscreen) + */ + stopKeyboardControl() { + this.state.keyboardEnabled = false; + } + + /** + * Creates a new tooltip + * @description Use {@link Tooltip.move} to update the tooltip without re-create + * @throws {@link PSVError} if the configuration is invalid + */ + createTooltip(config: TooltipConfig): Tooltip { + return new Tooltip(this, config); + } + + /** + * Subscribes to events on objects in the three.js scene + * @param userDataKey - only objects with the following `userData` will be observed + */ + observeObjects(userDataKey: string): void { + this.state.objectsObservers[userDataKey] = null; + } + + /** + * Unsubscribes to events on objects + */ + unobserveObjects(userDataKey: string): void { + delete this.state.objectsObservers[userDataKey]; + } + + /** + * Stops all current animations + * @internal + */ + stopAll(): PromiseLike { + this.dispatchEvent(new StopAllEvent()); + + this.disableIdleTimer(); + + return this.stopAnimation(); + } +} diff --git a/packages/core/src/adapters/AbstractAdapter.ts b/packages/core/src/adapters/AbstractAdapter.ts new file mode 100644 index 000000000..2a9a905e6 --- /dev/null +++ b/packages/core/src/adapters/AbstractAdapter.ts @@ -0,0 +1,161 @@ +import { Mesh, ShaderMaterial, ShaderMaterialParameters, Texture } from 'three'; +import { PanoData, PanoDataProvider, TextureData } from '../model'; +import type { Viewer } from '../Viewer'; + +/** + * Base class for adapters + * @template TPanorama type of the panorama object + * @template TTexture type of the loaded texture + */ +export abstract class AbstractAdapter { + /** + * Unique identifier of the adapter + */ + static readonly id: string; + + /** + * Indicates if the adapter supports panorama download natively + */ + static readonly supportsDownload: boolean = false; + + /** + * Indicates if the adapter can display an additional transparent image above the panorama + */ + static readonly supportsOverlay: boolean = false; + + constructor(protected readonly viewer: Viewer) {} + + /** + * Destroys the adapter + */ + // eslint-disable-next-line @typescript-eslint/no-empty-function + destroy(): void {} + + /** + * Indicates if the adapter supports transitions between panoramas + */ + // @ts-ignore unused paramater + // eslint-disable-next-line @typescript-eslint/no-unused-vars + supportsTransition(panorama: TPanorama): boolean { + return false; + } + + /** + * Indicates if the adapter supports preload of a panorama + */ + // @ts-ignore unused paramater + // eslint-disable-next-line @typescript-eslint/no-unused-vars + supportsPreload(panorama: TPanorama): boolean { + return false; + } + + /** + * Loads the panorama texture + */ + abstract loadTexture( + panorama: TPanorama, + newPanoData?: PanoData | PanoDataProvider, + useXmpPanoData?: boolean + ): Promise>; + + /** + * Creates the mesh + */ + abstract createMesh(scale?: number): Mesh; + + /** + * Applies the texture to the mesh + */ + abstract setTexture(mesh: Mesh, textureData: TextureData, transition?: boolean): void; + + /** + * Changes the opacity of the mesh + */ + abstract setTextureOpacity(mesh: Mesh, opacity: number): void; + + /** + * Clear a loaded texture from memory + */ + abstract disposeTexture(textureData: TextureData): void; + + /** + * Applies the overlay to the mesh + */ + abstract setOverlay(mesh: Mesh, textureData: TextureData, opacity: number): void; + + /** + * @internal + */ + static OVERLAY_UNIFORMS = { + panorama: 'panorama', + overlay: 'overlay', + globalOpacity: 'globalOpacity', + overlayOpacity: 'overlayOpacity', + }; + + /** + * @internal + */ + static createOverlayMaterial({ + additionalUniforms, + overrideVertexShader, + }: { + additionalUniforms?: ShaderMaterialParameters['uniforms']; + overrideVertexShader?: ShaderMaterialParameters['vertexShader']; + } = {}): ShaderMaterial { + return new ShaderMaterial({ + uniforms: { + ...additionalUniforms, + [AbstractAdapter.OVERLAY_UNIFORMS.panorama]: { value: new Texture() }, + [AbstractAdapter.OVERLAY_UNIFORMS.overlay]: { value: new Texture() }, + [AbstractAdapter.OVERLAY_UNIFORMS.globalOpacity]: { value: 1.0 }, + [AbstractAdapter.OVERLAY_UNIFORMS.overlayOpacity]: { value: 1.0 }, + }, + + vertexShader: + overrideVertexShader || + ` +varying vec2 vUv; + +void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); +}`, + + fragmentShader: ` +uniform sampler2D ${AbstractAdapter.OVERLAY_UNIFORMS.panorama}; +uniform sampler2D ${AbstractAdapter.OVERLAY_UNIFORMS.overlay}; +uniform float ${AbstractAdapter.OVERLAY_UNIFORMS.globalOpacity}; +uniform float ${AbstractAdapter.OVERLAY_UNIFORMS.overlayOpacity}; + +varying vec2 vUv; + +void main() { + vec4 tColor1 = texture2D( ${AbstractAdapter.OVERLAY_UNIFORMS.panorama}, vUv ); + vec4 tColor2 = texture2D( ${AbstractAdapter.OVERLAY_UNIFORMS.overlay}, vUv ); + gl_FragColor = vec4( + mix( tColor1.rgb, tColor2.rgb, tColor2.a * ${AbstractAdapter.OVERLAY_UNIFORMS.overlayOpacity} ), + ${AbstractAdapter.OVERLAY_UNIFORMS.globalOpacity} + ); +}`, + }); + } +} + +// prettier-ignore +export type AdapterConstructor = (new (viewer: Viewer, config?: any) => AbstractAdapter) & typeof AbstractAdapter; + +/** + * Returns the adapter constructor from the imported object + * @internal + */ +export function adapterInterop(adapter: any): AdapterConstructor { + if (adapter) { + for (const [, p] of [['_', adapter], ...Object.entries(adapter)]) { + if (p.prototype instanceof AbstractAdapter) { + return p; + } + } + } + return null; +} diff --git a/packages/core/src/adapters/EquirectangularAdapter.ts b/packages/core/src/adapters/EquirectangularAdapter.ts new file mode 100644 index 000000000..90d38c085 --- /dev/null +++ b/packages/core/src/adapters/EquirectangularAdapter.ts @@ -0,0 +1,250 @@ +import { BufferGeometry, MathUtils, Mesh, ShaderMaterial, SphereGeometry, Texture } from 'three'; +import { SPHERE_RADIUS } from '../data/constants'; +import { SYSTEM } from '../data/system'; +import { PanoData, PanoDataProvider, TextureData } from '../model'; +import { PSVError } from '../PSVError'; +import { createTexture, firstNonNull, getConfigParser, getXMPValue, logWarn } from '../utils'; +import type { Viewer } from '../Viewer'; +import { AbstractAdapter } from './AbstractAdapter'; + +/** + * Configuration for {@link EquirectangularAdapter} + */ +export type EquirectangularAdapterConfig = { + /** + * number of faces of the sphere geometry, higher values may decrease performances + * @default 64 + */ + resolution?: number; + /** + * used for equirectangular tiles adapter + * @internal + */ + blur?: boolean; +}; + +type EquirectangularMesh = Mesh; +type EquirectangularTexture = TextureData; + +const getConfig = getConfigParser( + { + resolution: 64, + blur: false, + }, + { + resolution: (resolution) => { + if (!resolution || !MathUtils.isPowerOfTwo(resolution)) { + throw new PSVError('EquirectangularAdapter resolution must be power of two'); + } + return resolution; + }, + } +); + +/** + * Adapter for equirectangular panoramas + */ +export class EquirectangularAdapter extends AbstractAdapter { + static override readonly id: string = 'equirectangular'; + static override readonly supportsDownload: boolean = true; + static override readonly supportsOverlay: boolean = true; + + private readonly config: EquirectangularAdapterConfig; + + private readonly SPHERE_SEGMENTS: number; + private readonly SPHERE_HORIZONTAL_SEGMENTS: number; + + constructor(viewer: Viewer, config?: EquirectangularAdapterConfig) { + super(viewer); + + this.config = getConfig(config); + + this.SPHERE_SEGMENTS = this.config.resolution; + this.SPHERE_HORIZONTAL_SEGMENTS = this.SPHERE_SEGMENTS / 2; + } + + override supportsTransition() { + return true; + } + + override supportsPreload() { + return true; + } + + async loadTexture( + panorama: string, + newPanoData: PanoData | PanoDataProvider, + useXmpPanoData = this.viewer.config.useXmpData + ): Promise { + if (typeof panorama !== 'string') { + return Promise.reject(new PSVError('Invalid panorama url, are you using the right adapter?')); + } + + let img: HTMLImageElement; + let xmpPanoData: PanoData; + + if (useXmpPanoData) { + xmpPanoData = await this.loadXMP(panorama, (p) => this.viewer.loader.setProgress(p)); + img = await this.viewer.textureLoader.loadImage(panorama); + } else { + img = await this.viewer.textureLoader.loadImage(panorama, (p) => this.viewer.loader.setProgress(p)); + } + + if (typeof newPanoData === 'function') { + newPanoData = newPanoData(img); + } + + const panoData = { + fullWidth: firstNonNull(newPanoData?.fullWidth, xmpPanoData?.fullWidth, img.width), + fullHeight: firstNonNull(newPanoData?.fullHeight, xmpPanoData?.fullHeight, img.height), + croppedWidth: firstNonNull(newPanoData?.croppedWidth, xmpPanoData?.croppedWidth, img.width), + croppedHeight: firstNonNull(newPanoData?.croppedHeight, xmpPanoData?.croppedHeight, img.height), + croppedX: firstNonNull(newPanoData?.croppedX, xmpPanoData?.croppedX, 0), + croppedY: firstNonNull(newPanoData?.croppedY, xmpPanoData?.croppedY, 0), + poseHeading: firstNonNull(newPanoData?.poseHeading, xmpPanoData?.poseHeading, 0), + posePitch: firstNonNull(newPanoData?.posePitch, xmpPanoData?.posePitch, 0), + poseRoll: firstNonNull(newPanoData?.poseRoll, xmpPanoData?.poseRoll, 0), + }; + + if (panoData.croppedWidth !== img.width || panoData.croppedHeight !== img.height) { + logWarn(`Invalid panoData, croppedWidth and/or croppedHeight is not coherent with loaded image. + panoData: ${panoData.croppedWidth}x${panoData.croppedHeight}, image: ${img.width}x${img.height}`); + } + if ((newPanoData || xmpPanoData) && panoData.fullWidth !== panoData.fullHeight * 2) { + logWarn('Invalid panoData, fullWidth should be twice fullHeight'); + } + + const texture = this.createEquirectangularTexture(img, panoData); + + return { panorama, texture, panoData }; + } + + /** + * Loads the XMP data of an image + */ + private async loadXMP(panorama: string, onProgress?: (p: number) => void): Promise { + const blob = await this.viewer.textureLoader.loadFile(panorama, onProgress); + const binary = await this.loadBlobAsString(blob); + + const a = binary.indexOf(''); + const data = binary.substring(a, b); + + if (a !== -1 && b !== -1 && data.includes('GPano:')) { + return { + fullWidth: getXMPValue(data, 'FullPanoWidthPixels'), + fullHeight: getXMPValue(data, 'FullPanoHeightPixels'), + croppedWidth: getXMPValue(data, 'CroppedAreaImageWidthPixels'), + croppedHeight: getXMPValue(data, 'CroppedAreaImageHeightPixels'), + croppedX: getXMPValue(data, 'CroppedAreaLeftPixels'), + croppedY: getXMPValue(data, 'CroppedAreaTopPixels'), + poseHeading: getXMPValue(data, 'PoseHeadingDegrees'), + posePitch: getXMPValue(data, 'PosePitchDegrees'), + poseRoll: getXMPValue(data, 'PoseRollDegrees'), + }; + } + + return null; + } + + /** + * Reads a Blob as a string + */ + private loadBlobAsString(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsText(blob); + }); + } + + /** + * Creates the final texture from image and panorama data + */ + private createEquirectangularTexture(img: HTMLImageElement, panoData: PanoData): Texture { + // resize image / fill cropped parts with black + if (this.config.blur + || panoData.fullWidth > SYSTEM.maxTextureWidth + || panoData.croppedWidth !== panoData.fullWidth + || panoData.croppedHeight !== panoData.fullHeight + ) { + const ratio = Math.min(1, SYSTEM.maxCanvasWidth / panoData.fullWidth); + + const resizedPanoData = { + fullWidth: panoData.fullWidth * ratio, + fullHeight: panoData.fullHeight * ratio, + croppedWidth: panoData.croppedWidth * ratio, + croppedHeight: panoData.croppedHeight * ratio, + croppedX: panoData.croppedX * ratio, + croppedY: panoData.croppedY * ratio, + }; + + const buffer = document.createElement('canvas'); + buffer.width = resizedPanoData.fullWidth; + buffer.height = resizedPanoData.fullHeight; + + const ctx = buffer.getContext('2d'); + + if (this.config.blur) { + ctx.filter = 'blur(1px)'; + } + + ctx.drawImage( + img, + resizedPanoData.croppedX, + resizedPanoData.croppedY, + resizedPanoData.croppedWidth, + resizedPanoData.croppedHeight + ); + + return createTexture(buffer); + } + + return createTexture(img); + } + + createMesh(scale = 1): EquirectangularMesh { + // The middle of the panorama is placed at yaw=0 + const geometry = new SphereGeometry( + SPHERE_RADIUS * scale, + this.SPHERE_SEGMENTS, + this.SPHERE_HORIZONTAL_SEGMENTS, + -Math.PI / 2 + ).scale(-1, 1, 1) as SphereGeometry; + + const material = AbstractAdapter.createOverlayMaterial(); + + return new Mesh(geometry, material); + } + + setTexture(mesh: EquirectangularMesh, textureData: EquirectangularTexture) { + this.__setUniform(mesh, AbstractAdapter.OVERLAY_UNIFORMS.panorama, textureData.texture); + this.setOverlay(mesh, null, 1); + } + + setOverlay(mesh: EquirectangularMesh, textureData: EquirectangularTexture, opacity: number) { + this.__setUniform(mesh, AbstractAdapter.OVERLAY_UNIFORMS.overlayOpacity, opacity); + if (!textureData) { + this.__setUniform(mesh, AbstractAdapter.OVERLAY_UNIFORMS.overlay, new Texture()); + } else { + this.__setUniform(mesh, AbstractAdapter.OVERLAY_UNIFORMS.overlay, textureData.texture); + } + } + + setTextureOpacity(mesh: EquirectangularMesh, opacity: number) { + this.__setUniform(mesh, AbstractAdapter.OVERLAY_UNIFORMS.globalOpacity, opacity); + mesh.material.transparent = opacity < 1; + } + + disposeTexture(textureData: EquirectangularTexture) { + textureData.texture?.dispose(); + } + + private __setUniform(mesh: EquirectangularMesh, uniform: string, value: any) { + if (mesh.material.uniforms[uniform].value instanceof Texture) { + mesh.material.uniforms[uniform].value.dispose(); + } + mesh.material.uniforms[uniform].value = value; + } +} diff --git a/packages/core/src/buttons/AbstractButton.ts b/packages/core/src/buttons/AbstractButton.ts new file mode 100644 index 000000000..47fb45396 --- /dev/null +++ b/packages/core/src/buttons/AbstractButton.ts @@ -0,0 +1,260 @@ +import { AbstractComponent } from '../components/AbstractComponent'; +import type { Navbar } from '../components/Navbar'; +import { KEY_CODES } from '../data/constants'; +import { InitialPromise } from '../model'; +import { addClasses, getConfigParser, isPlainObject, toggleClass } from '../utils'; + +/** + * Configuration for {@link AbstractButton} + */ +export type ButtonConfig = { + id?: string; + className?: string; + title?: string; + /** + * if the button has an mouse hover effect + * @default false + */ + hoverScale?: boolean; + /** + * if the button can be moved to menu when the navbar is too small + * @default false + */ + collapsable?: boolean; + /** + * if the button is accessible with the keyboard + * @default true + */ + tabbable?: boolean; + /** + * icon of the button + */ + icon?: string; + /** + * override icon when the button is active + */ + iconActive?: string; +}; + +const getConfig = getConfigParser({ + id: null, + className: null, + title: null, + hoverScale: false, + collapsable: false, + tabbable: true, + icon: null, + iconActive: null, +}); + +/** + * Base class for navbar buttons + */ +export abstract class AbstractButton extends AbstractComponent { + /** + * Unique identifier of the button + */ + static readonly id: string; + + /** + * Identifier to declare a group of buttons + */ + static readonly groupId?: string; + + /** + * Internal properties + */ + protected override readonly state = { + visible: true, + enabled: true, + supported: true, + collapsed: false, + active: false, + width: 0, + }; + + protected readonly config: ButtonConfig; + + get id(): string { + return this.config.id; + } + + get title(): string { + return this.container.title; + } + + get content(): string { + return this.container.innerHTML; + } + + get width(): number { + return this.state.width; + } + + get collapsable(): boolean { + return this.config.collapsable; + } + + constructor(navbar: Navbar, config: ButtonConfig) { + super(navbar, { + className: `psv-button ${config.hoverScale ? 'psv-button--hover-scale' : ''} ${config.className || ''}`, + }); + + this.config = getConfig(config); + this.config.id = (this.constructor as typeof AbstractButton).id; + + if (config.icon) { + this.__setIcon(config.icon); + } + + this.state.width = this.container.offsetWidth; + + if (this.config.title) { + this.container.title = this.config.title; + } else if (this.id && this.id in this.viewer.config.lang) { + this.container.title = (this.viewer.config.lang as any)[this.id]; + } + + if (config.tabbable) { + this.container.tabIndex = 0; + } + + this.container.addEventListener('click', (e) => { + if (this.state.enabled) { + this.onClick(); + } + e.stopPropagation(); + }); + + this.container.addEventListener('keydown', (e) => { + if (e.key === KEY_CODES.Enter && this.state.enabled) { + this.onClick(); + e.stopPropagation(); + } + }); + } + + /** + * Action when the button is clicked + */ + abstract onClick(): void; + + override show(refresh = true) { + if (!this.isVisible()) { + this.state.visible = true; + if (!this.state.collapsed) { + this.container.style.display = ''; + } + if (refresh) { + this.viewer.navbar.autoSize(); + } + } + } + + override hide(refresh = true) { + if (this.isVisible()) { + this.state.visible = false; + this.container.style.display = 'none'; + if (refresh) { + this.viewer.navbar.autoSize(); + } + } + } + + /** + * Hides/shows the button depending of the result of {@link isSupported} + * @internal + */ + checkSupported() { + const supportedOrObject = this.isSupported(); + if (isPlainObject(supportedOrObject)) { + if (supportedOrObject.initial === false) { + this.state.supported = false; + this.hide(); + } + + supportedOrObject.promise.then((supported) => { + if (!this.state) { + return; // the component has been destroyed + } + this.state.supported = supported; + this.toggle(supported); + }); + } else { + this.state.supported = supportedOrObject; + if (!supportedOrObject) { + this.hide(); + } + } + } + + /** + * Perform action when the navbar size/content changes + * @internal + */ + autoSize() { + // nothing + } + + /** + * Checks if the button can be displayed + */ + isSupported(): boolean | InitialPromise { + return true; + } + + /** + * Changes the active state of the button + */ + toggleActive(active = !this.state.active) { + if (active !== this.state.active) { + this.state.active = active; + toggleClass(this.container, 'psv-button--active', this.state.active); + + if (this.config.iconActive) { + this.__setIcon(this.state.active ? this.config.iconActive : this.config.icon); + } + } + } + + /** + * Disables the button + */ + disable() { + this.container.classList.add('psv-button--disabled'); + this.state.enabled = false; + } + + /** + * Enables the button + */ + enable() { + this.container.classList.remove('psv-button--disabled'); + this.state.enabled = true; + } + + /** + * Collapses the button in the navbar menu + */ + collapse() { + this.state.collapsed = true; + this.container.style.display = 'none'; + } + + /** + * Uncollapses the button from the navbar menu + */ + uncollapse() { + this.state.collapsed = false; + if (this.state.visible) { + this.container.style.display = ''; + } + } + + private __setIcon(icon: string) { + this.container.innerHTML = icon; + addClasses(this.container.querySelector('svg'), 'psv-button-svg'); + } +} + +export type ButtonConstructor = { new (navbar: Navbar): AbstractButton } & typeof AbstractButton; diff --git a/packages/core/src/buttons/AbstractMoveButton.ts b/packages/core/src/buttons/AbstractMoveButton.ts new file mode 100644 index 000000000..d11e5d3b3 --- /dev/null +++ b/packages/core/src/buttons/AbstractMoveButton.ts @@ -0,0 +1,112 @@ +import type { Navbar } from '../components/Navbar'; +import { KEY_CODES } from '../data/constants'; +import { SYSTEM } from '../data/system'; +import icon from '../icons/arrow.svg'; +import { PressHandler } from '../utils/PressHandler'; +import { AbstractButton } from './AbstractButton'; + +export const enum MoveButtonDirection { + UP, + DOWN, + LEFT, + RIGHT, +} + +function getIcon(value: MoveButtonDirection): string { + let angle = 0; + // prettier-ignore + switch (value) { + case MoveButtonDirection.UP: angle = 90; break; + case MoveButtonDirection.DOWN: angle = -90; break; + case MoveButtonDirection.RIGHT: angle = 180; break; + default: angle = 0; break; + } + + return icon.replace('rotate(0', `rotate(${angle}`); +} + +export abstract class AbstractMoveButton extends AbstractButton { + static override readonly groupId = 'move'; + + private readonly handler = new PressHandler(); + + constructor(navbar: Navbar, private direction: MoveButtonDirection) { + super(navbar, { + className: 'psv-move-button', + hoverScale: true, + collapsable: false, + tabbable: true, + icon: getIcon(direction), + }); + + this.container.addEventListener('mousedown', this); + this.container.addEventListener('keydown', this); + this.container.addEventListener('keyup', this); + this.viewer.container.addEventListener('mouseup', this); + this.viewer.container.addEventListener('touchend', this); + } + + override destroy() { + this.__onMouseUp(); + + this.viewer.container.removeEventListener('mouseup', this); + this.viewer.container.removeEventListener('touchend', this); + + super.destroy(); + } + + handleEvent(e: Event) { + // prettier-ignore + switch (e.type) { + case 'mousedown': this.__onMouseDown(); break; + case 'mouseup': this.__onMouseUp(); break; + case 'touchend': this.__onMouseUp(); break; + case 'keydown': (e as KeyboardEvent).key === KEY_CODES.Enter && this.__onMouseDown(); break; + case 'keyup': (e as KeyboardEvent).key === KEY_CODES.Enter && this.__onMouseUp(); break; + } + } + + onClick() { + // nothing + } + + override isSupported() { + return { + initial: !SYSTEM.isTouchEnabled.initial, + promise: SYSTEM.isTouchEnabled.promise.then((enabled) => !enabled), + }; + } + + private __onMouseDown() { + if (!this.state.enabled) { + return; + } + + const dynamicRoll: { + yaw?: boolean; + pitch?: boolean; + } = {}; + // prettier-ignore + switch (this.direction) { + case MoveButtonDirection.UP: dynamicRoll.pitch = false; break; + case MoveButtonDirection.DOWN: dynamicRoll.pitch = true; break; + case MoveButtonDirection.RIGHT: dynamicRoll.yaw = false; break; + default: dynamicRoll.yaw = true; break; + } + + this.viewer.stopAll(); + this.viewer.dynamics.position.roll(dynamicRoll); + this.handler.down(); + } + + private __onMouseUp() { + if (!this.state.enabled) { + return; + } + + this.handler.up(() => { + this.viewer.dynamics.position.stop(); + this.viewer.resetIdleTimer(); + }); + } +} diff --git a/packages/core/src/buttons/AbstractZoomButton.ts b/packages/core/src/buttons/AbstractZoomButton.ts new file mode 100644 index 000000000..d7f497d22 --- /dev/null +++ b/packages/core/src/buttons/AbstractZoomButton.ts @@ -0,0 +1,80 @@ +import type { Navbar } from '../components/Navbar'; +import { KEY_CODES } from '../data/constants'; +import { SYSTEM } from '../data/system'; +import { PressHandler } from '../utils/PressHandler'; +import { AbstractButton } from './AbstractButton'; + +export const enum ZoomButtonDirection { + IN, + OUT, +} + +export class AbstractZoomButton extends AbstractButton { + static override readonly groupId = 'zoom'; + + private readonly handler = new PressHandler(); + + constructor(navbar: Navbar, private direction: ZoomButtonDirection, icon: string) { + super(navbar, { + className: 'psv-zoom-button', + hoverScale: true, + collapsable: false, + tabbable: true, + icon: icon, + }); + + this.container.addEventListener('mousedown', this); + this.container.addEventListener('keydown', this); + this.container.addEventListener('keyup', this); + this.viewer.container.addEventListener('mouseup', this); + this.viewer.container.addEventListener('touchend', this); + } + + override destroy() { + this.__onMouseUp(); + + this.viewer.container.removeEventListener('mouseup', this); + this.viewer.container.removeEventListener('touchend', this); + + super.destroy(); + } + + handleEvent(e: Event) { + // prettier-ignore + switch (e.type) { + case 'mousedown': this.__onMouseDown(); break; + case 'mouseup': this.__onMouseUp(); break; + case 'touchend': this.__onMouseUp(); break; + case 'keydown': (e as KeyboardEvent).key === KEY_CODES.Enter && this.__onMouseDown(); break; + case 'keyup': (e as KeyboardEvent).key === KEY_CODES.Enter && this.__onMouseUp(); break; + } + } + + onClick() { + // nothing + } + + override isSupported() { + return { + initial: !SYSTEM.isTouchEnabled.initial, + promise: SYSTEM.isTouchEnabled.promise.then((enabled) => !enabled), + }; + } + + private __onMouseDown() { + if (!this.state.enabled) { + return; + } + + this.viewer.dynamics.zoom.roll(this.direction === ZoomButtonDirection.OUT); + this.handler.down(); + } + + private __onMouseUp() { + if (!this.state.enabled) { + return; + } + + this.handler.up(() => this.viewer.dynamics.zoom.stop()); + } +} diff --git a/packages/core/src/buttons/CustomButton.ts b/packages/core/src/buttons/CustomButton.ts new file mode 100644 index 000000000..1b5086d33 --- /dev/null +++ b/packages/core/src/buttons/CustomButton.ts @@ -0,0 +1,43 @@ +import type { Navbar } from '../components/Navbar'; +import { NavbarCustomButton } from '../model'; +import { AbstractButton } from './AbstractButton'; + +export class CustomButton extends AbstractButton { + private readonly customOnClick: NavbarCustomButton['onClick']; + + constructor(navbar: Navbar, config: NavbarCustomButton) { + super(navbar, { + className: `psv-custom-button ${config.className || ''}`, + hoverScale: false, + collapsable: config.collapsable !== false, + tabbable: config.tabbable !== false, + title: config.title, + }); + + this.customOnClick = config.onClick; + + if (config.id) { + this.config.id = config.id; + } else { + this.config.id = 'psvButton-' + Math.random().toString(36).substring(2); + } + + if (config.content) { + this.container.innerHTML = config.content; + } + + this.state.width = this.container.offsetWidth; + + if (config.disabled) { + this.disable(); + } + + if (config.visible === false) { + this.hide(); + } + } + + onClick() { + this.customOnClick?.(this.viewer); + } +} diff --git a/packages/core/src/buttons/DescriptionButton.ts b/packages/core/src/buttons/DescriptionButton.ts new file mode 100644 index 000000000..4fb8f79c8 --- /dev/null +++ b/packages/core/src/buttons/DescriptionButton.ts @@ -0,0 +1,142 @@ +import type { Navbar } from '../components/Navbar'; +import { IDS } from '../data/constants'; +import icon from '../icons/info.svg'; +import { + ConfigChangedEvent, + HideNotificationEvent, + HidePanelEvent, + ShowNotificationEvent, + ShowPanelEvent, +} from '../events'; +import { AbstractButton } from './AbstractButton'; + +const enum DescriptionButtonMode { + NONE, + NOTIF, + PANEL, +} + +export class DescriptionButton extends AbstractButton { + static override readonly id = 'description'; + + private mode = DescriptionButtonMode.NONE; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-description-button', + hoverScale: true, + collapsable: false, + tabbable: true, + icon: icon, + }); + + this.viewer.addEventListener(HideNotificationEvent.type, this); + this.viewer.addEventListener(ShowNotificationEvent.type, this); + this.viewer.addEventListener(HidePanelEvent.type, this); + this.viewer.addEventListener(ShowPanelEvent.type, this); + this.viewer.addEventListener(ConfigChangedEvent.type, this); + } + + override destroy() { + this.viewer.removeEventListener(HideNotificationEvent.type, this); + this.viewer.removeEventListener(ShowNotificationEvent.type, this); + this.viewer.removeEventListener(HidePanelEvent.type, this); + this.viewer.removeEventListener(ShowPanelEvent.type, this); + this.viewer.removeEventListener(ConfigChangedEvent.type, this); + + super.destroy(); + } + + handleEvent(e: Event) { + if (e instanceof ConfigChangedEvent) { + e.containsOptions('description') && this.autoSize(true); + return; + } + + if (!this.mode) { + return; + } + + let closed = false; + if (e instanceof HideNotificationEvent) { + closed = this.mode === DescriptionButtonMode.NOTIF; + } else if (e instanceof ShowNotificationEvent) { + closed = this.mode === DescriptionButtonMode.NOTIF && e.notificationId !== IDS.DESCRIPTION; + } else if (e instanceof HidePanelEvent) { + closed = this.mode === DescriptionButtonMode.PANEL; + } else if (e instanceof ShowPanelEvent) { + closed = this.mode === DescriptionButtonMode.PANEL && e.panelId !== IDS.DESCRIPTION; + } + + if (closed) { + this.toggleActive(false); + this.mode = DescriptionButtonMode.NONE; + } + } + + onClick() { + if (this.mode) { + this.__close(); + } else { + this.__open(); + } + } + + override hide(refresh?: boolean) { + super.hide(refresh); + + if (this.mode) { + this.__close(); + } + } + + /** + * This button can only be refreshed from NavbarCaption + * @internal + */ + override autoSize(refresh = false) { + if (refresh) { + const caption = this.viewer.navbar.getButton('caption', false); + const captionHidden = caption && !caption.isVisible(); + const hasDescription = !!this.viewer.config.description; + + if (captionHidden || hasDescription) { + this.show(false); + } else { + this.hide(false); + } + } + } + + private __close() { + switch (this.mode) { + case DescriptionButtonMode.NOTIF: + this.viewer.notification.hide(IDS.DESCRIPTION); + break; + case DescriptionButtonMode.PANEL: + this.viewer.panel.hide(IDS.DESCRIPTION); + break; + default: + } + } + + private __open() { + this.toggleActive(true); + + if (this.viewer.config.description) { + this.mode = DescriptionButtonMode.PANEL; + this.viewer.panel.show({ + id: IDS.DESCRIPTION, + content: + (this.viewer.config.caption ? `

${this.viewer.config.caption}

` : '') + + this.viewer.config.description, + }); + } else { + this.mode = DescriptionButtonMode.NOTIF; + this.viewer.notification.show({ + id: IDS.DESCRIPTION, + content: this.viewer.config.caption, + }); + } + } +} diff --git a/packages/core/src/buttons/DownloadButton.ts b/packages/core/src/buttons/DownloadButton.ts new file mode 100644 index 000000000..8cace7a14 --- /dev/null +++ b/packages/core/src/buttons/DownloadButton.ts @@ -0,0 +1,56 @@ +import { AbstractAdapter } from '../adapters/AbstractAdapter'; +import type { Navbar } from '../components/Navbar'; +import icon from '../icons/download.svg'; +import { ConfigChangedEvent } from '../events'; +import { AbstractButton } from './AbstractButton'; + +export class DownloadButton extends AbstractButton { + static override readonly id = 'download'; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-download-butto', + hoverScale: true, + collapsable: true, + tabbable: true, + icon: icon, + }); + + this.viewer.addEventListener(ConfigChangedEvent.type, this); + } + + override destroy(): void { + this.viewer.removeEventListener(ConfigChangedEvent.type, this); + + super.destroy(); + } + + handleEvent(e: Event) { + if (e instanceof ConfigChangedEvent) { + e.containsOptions('downloadUrl') && this.checkSupported(); + } + } + + onClick() { + const link = document.createElement('a'); + link.href = this.viewer.config.downloadUrl || this.viewer.config.panorama; + link.download = link.href.split('/').pop(); + this.viewer.container.appendChild(link); + link.click(); + + setTimeout(() => { + this.viewer.container.removeChild(link); + }, 100); + } + + override checkSupported() { + const supported = + (this.viewer.adapter.constructor as typeof AbstractAdapter).supportsDownload || + this.viewer.config.downloadUrl; + if (supported) { + this.show(); + } else { + this.hide(); + } + } +} diff --git a/packages/core/src/buttons/FullscreenButton.ts b/packages/core/src/buttons/FullscreenButton.ts new file mode 100644 index 000000000..4594cb1de --- /dev/null +++ b/packages/core/src/buttons/FullscreenButton.ts @@ -0,0 +1,38 @@ +import type { Navbar } from '../components/Navbar'; +import icon from '../icons/fullscreen-in.svg'; +import iconActive from '../icons/fullscreen-out.svg'; +import { FullscreenEvent } from '../events'; +import { AbstractButton } from './AbstractButton'; + +export class FullscreenButton extends AbstractButton { + static override readonly id = 'fullscreen'; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-fullscreen-button', + hoverScale: true, + collapsable: false, + tabbable: true, + icon: icon, + iconActive: iconActive, + }); + + this.viewer.addEventListener(FullscreenEvent.type, this); + } + + override destroy() { + this.viewer.removeEventListener(FullscreenEvent.type, this); + + super.destroy(); + } + + handleEvent(e: Event) { + if (e instanceof FullscreenEvent) { + this.toggleActive(e.fullscreenEnabled); + } + } + + onClick() { + this.viewer.toggleFullscreen(); + } +} diff --git a/packages/core/src/buttons/MenuButton.ts b/packages/core/src/buttons/MenuButton.ts new file mode 100644 index 000000000..52f4d5c81 --- /dev/null +++ b/packages/core/src/buttons/MenuButton.ts @@ -0,0 +1,98 @@ +import type { Navbar } from '../components/Navbar'; +import { IDS } from '../data/constants'; +import icon from '../icons/menu.svg'; +import { getClosest } from '../utils'; +import { HidePanelEvent, ShowPanelEvent } from '../events'; +import { AbstractButton } from './AbstractButton'; + +const BUTTON_DATA = 'psvButton'; + +const MENU_TEMPLATE = (buttons: AbstractButton[], title: string) => ` +
+

${icon} ${title}

+
    + ${buttons.map((button) => ` +
  • + ${button.content} + ${button.title} +
  • + `).join('')} +
+
+`; + +export class MenuButton extends AbstractButton { + static override readonly id = 'menu'; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-menu-button', + hoverScale: true, + collapsable: false, + tabbable: true, + icon: icon, + }); + + this.viewer.addEventListener(ShowPanelEvent.type, this); + this.viewer.addEventListener(HidePanelEvent.type, this); + + super.hide(); + } + + override destroy() { + this.viewer.removeEventListener(ShowPanelEvent.type, this); + this.viewer.removeEventListener(HidePanelEvent.type, this); + + super.destroy(); + } + + handleEvent(e: Event) { + if (e instanceof ShowPanelEvent) { + this.toggleActive(e.panelId === IDS.MENU); + } else if (e instanceof HidePanelEvent) { + this.toggleActive(false); + } + } + + onClick() { + if (this.state.active) { + this.__hideMenu(); + } else { + this.__showMenu(); + } + } + + override hide(refresh?: boolean) { + super.hide(refresh); + this.__hideMenu(); + } + + override show(refresh?: boolean) { + super.show(refresh); + + if (this.state.active) { + this.__showMenu(); + } + } + + private __showMenu() { + this.viewer.panel.show({ + id: IDS.MENU, + content: MENU_TEMPLATE(this.viewer.navbar.collapsed, this.viewer.config.lang.menu), + noMargin: true, + clickHandler: (target) => { + const li = target ? getClosest(target as HTMLElement, 'li') : undefined; + const buttonId = li ? li.dataset[BUTTON_DATA] : undefined; + + if (buttonId) { + this.viewer.navbar.getButton(buttonId).onClick(); + this.__hideMenu(); + } + }, + }); + } + + private __hideMenu() { + this.viewer.panel.hide(IDS.MENU); + } +} diff --git a/packages/core/src/buttons/MoveDownButton.ts b/packages/core/src/buttons/MoveDownButton.ts new file mode 100644 index 000000000..7abeceee0 --- /dev/null +++ b/packages/core/src/buttons/MoveDownButton.ts @@ -0,0 +1,10 @@ +import type { Navbar } from '../components/Navbar'; +import { AbstractMoveButton, MoveButtonDirection } from './AbstractMoveButton'; + +export class MoveDownButton extends AbstractMoveButton { + static override readonly id = 'moveDown'; + + constructor(navbar: Navbar) { + super(navbar, MoveButtonDirection.DOWN); + } +} diff --git a/packages/core/src/buttons/MoveLeftButton.ts b/packages/core/src/buttons/MoveLeftButton.ts new file mode 100644 index 000000000..ebe84a4c0 --- /dev/null +++ b/packages/core/src/buttons/MoveLeftButton.ts @@ -0,0 +1,10 @@ +import type { Navbar } from '../components/Navbar'; +import { AbstractMoveButton, MoveButtonDirection } from './AbstractMoveButton'; + +export class MoveLeftButton extends AbstractMoveButton { + static override readonly id = 'moveLeft'; + + constructor(navbar: Navbar) { + super(navbar, MoveButtonDirection.LEFT); + } +} diff --git a/packages/core/src/buttons/MoveRightButton.ts b/packages/core/src/buttons/MoveRightButton.ts new file mode 100644 index 000000000..8c43aee0d --- /dev/null +++ b/packages/core/src/buttons/MoveRightButton.ts @@ -0,0 +1,10 @@ +import type { Navbar } from '../components/Navbar'; +import { AbstractMoveButton, MoveButtonDirection } from './AbstractMoveButton'; + +export class MoveRightButton extends AbstractMoveButton { + static override readonly id = 'moveRight'; + + constructor(navbar: Navbar) { + super(navbar, MoveButtonDirection.RIGHT); + } +} diff --git a/packages/core/src/buttons/MoveUpButton.ts b/packages/core/src/buttons/MoveUpButton.ts new file mode 100644 index 000000000..0179351ee --- /dev/null +++ b/packages/core/src/buttons/MoveUpButton.ts @@ -0,0 +1,10 @@ +import type { Navbar } from '../components/Navbar'; +import { AbstractMoveButton, MoveButtonDirection } from './AbstractMoveButton'; + +export class MoveUpButton extends AbstractMoveButton { + static override readonly id = 'moveUp'; + + constructor(navbar: Navbar) { + super(navbar, MoveButtonDirection.UP); + } +} diff --git a/packages/core/src/buttons/ZoomInButton.ts b/packages/core/src/buttons/ZoomInButton.ts new file mode 100644 index 000000000..1a8cf3474 --- /dev/null +++ b/packages/core/src/buttons/ZoomInButton.ts @@ -0,0 +1,11 @@ +import type { Navbar } from '../components/Navbar'; +import icon from '../icons/zoom-in.svg'; +import { AbstractZoomButton, ZoomButtonDirection } from './AbstractZoomButton'; + +export class ZoomInButton extends AbstractZoomButton { + static override readonly id = 'zoomIn'; + + constructor(navbar: Navbar) { + super(navbar, ZoomButtonDirection.IN, icon); + } +} diff --git a/packages/core/src/buttons/ZoomOutButton.ts b/packages/core/src/buttons/ZoomOutButton.ts new file mode 100644 index 000000000..839f56fe8 --- /dev/null +++ b/packages/core/src/buttons/ZoomOutButton.ts @@ -0,0 +1,11 @@ +import type { Navbar } from '../components/Navbar'; +import icon from '../icons/zoom-out.svg'; +import { AbstractZoomButton, ZoomButtonDirection } from './AbstractZoomButton'; + +export class ZoomOutButton extends AbstractZoomButton { + static override readonly id = 'zoomOut'; + + constructor(navbar: Navbar) { + super(navbar, ZoomButtonDirection.OUT, icon); + } +} diff --git a/packages/core/src/buttons/ZoomRangeButton.ts b/packages/core/src/buttons/ZoomRangeButton.ts new file mode 100644 index 000000000..18c37b151 --- /dev/null +++ b/packages/core/src/buttons/ZoomRangeButton.ts @@ -0,0 +1,91 @@ +import type { Navbar } from '../components/Navbar'; +import { SYSTEM } from '../data/system'; +import { ReadyEvent, ZoomUpdatedEvent } from '../events'; +import { getStyle, Slider, SliderDirection, SliderUpdateData } from '../utils'; +import { AbstractButton } from './AbstractButton'; + +export class ZoomRangeButton extends AbstractButton { + static override readonly id = 'zoomRange'; + static override readonly groupId = 'zoom'; + + private readonly slider: Slider; + private readonly zoomRange: HTMLElement; + private readonly zoomValue: HTMLElement; + private readonly mediaMinWidth: number; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-zoom-range', + hoverScale: false, + collapsable: false, + tabbable: false, + }); + + this.zoomRange = document.createElement('div'); + this.zoomRange.className = 'psv-zoom-range-line'; + this.container.appendChild(this.zoomRange); + + this.zoomValue = document.createElement('div'); + this.zoomValue.className = 'psv-zoom-range-handle'; + this.zoomRange.appendChild(this.zoomValue); + + this.slider = new Slider(this.container, SliderDirection.HORIZONTAL, (data) => this.__onSliderUpdate(data)); + + this.mediaMinWidth = parseInt(getStyle(this.container, 'maxWidth'), 10); + + this.viewer.addEventListener(ZoomUpdatedEvent.type, this); + if (this.viewer.state.ready) { + this.__moveZoomValue(this.viewer.getZoomLevel()); + } else { + this.viewer.addEventListener(ReadyEvent.type, this); + } + } + + override destroy() { + this.slider.destroy(); + + this.viewer.removeEventListener(ZoomUpdatedEvent.type, this); + this.viewer.removeEventListener(ReadyEvent.type, this); + + super.destroy(); + } + + handleEvent(e: Event) { + if (e instanceof ZoomUpdatedEvent) { + this.__moveZoomValue(e.zoomLevel); + } else if (e instanceof ReadyEvent) { + this.__moveZoomValue(this.viewer.getZoomLevel()); + } + } + + onClick() { + // nothing + } + + override isSupported() { + return { + initial: !SYSTEM.isTouchEnabled.initial, + promise: SYSTEM.isTouchEnabled.promise.then((enabled) => !enabled), + }; + } + + override autoSize() { + if (this.state.supported) { + if (this.viewer.state.size.width <= this.mediaMinWidth && this.state.visible) { + this.hide(false); + } else if (this.viewer.state.size.width > this.mediaMinWidth && !this.state.visible) { + this.show(false); + } + } + } + + private __moveZoomValue(level: number) { + this.zoomValue.style.left = (level / 100) * this.zoomRange.offsetWidth - this.zoomValue.offsetWidth / 2 + 'px'; + } + + private __onSliderUpdate(data: SliderUpdateData) { + if (data.mousedown) { + this.viewer.zoom(data.value * 100); + } + } +} diff --git a/packages/core/src/components/AbstractComponent.ts b/packages/core/src/components/AbstractComponent.ts new file mode 100644 index 000000000..0586d0124 --- /dev/null +++ b/packages/core/src/components/AbstractComponent.ts @@ -0,0 +1,92 @@ +import type { Viewer } from '../Viewer'; + +/** + * Base class for UI components + */ +export abstract class AbstractComponent { + /** + * Reference to main controller + */ + protected readonly viewer: Viewer; + + /** + * All child components + * @internal + */ + readonly children: AbstractComponent[] = []; + + /** + * Container element + */ + readonly container: HTMLDivElement = document.createElement('div'); + + /** + * Internal properties + * @internal + */ + protected readonly state = { + visible: true, + }; + + constructor(protected readonly parent: Viewer | AbstractComponent, config: { className: string }) { + this.viewer = parent instanceof AbstractComponent ? parent.viewer : parent; + + this.container.className = config.className; + + this.parent.children.push(this); + this.parent.container.appendChild(this.container); + } + + /** + * Destroys the component + */ + destroy() { + this.parent.container.removeChild(this.container); + + const childIdx = this.parent.children.indexOf(this); + if (childIdx !== -1) { + this.parent.children.splice(childIdx, 1); + } + + this.children.slice().forEach((child) => child.destroy()); + this.children.length = 0; + } + + /** + * Displays or hides the component + */ + toggle(visible = !this.isVisible()) { + if (!visible) { + this.hide(); + } else { + this.show(); + } + } + + /** + * Hides the component + */ + // @ts-ignore unused parameter + // eslint-disable-next-line @typescript-eslint/no-unused-vars + hide(options?: any) { + this.container.style.display = 'none'; + this.state.visible = false; + } + + /** + * Displays the component + */ + // @ts-ignore unused parameter + // eslint-disable-next-line @typescript-eslint/no-unused-vars + show(options?: any) { + this.container.style.display = ''; + this.state.visible = true; + } + + /** + * Checks if the component is visible + */ + isVisible(): boolean { + return this.state.visible; + } +} diff --git a/packages/core/src/components/Loader.ts b/packages/core/src/components/Loader.ts new file mode 100644 index 000000000..57847f935 --- /dev/null +++ b/packages/core/src/components/Loader.ts @@ -0,0 +1,112 @@ +import { ConfigChangedEvent, LoadProgressEvent } from '../events'; +import { getStyle } from '../utils'; +import type { Viewer } from '../Viewer'; +import { AbstractComponent } from './AbstractComponent'; + +/** + * Loader component + */ +export class Loader extends AbstractComponent { + private readonly loader: HTMLElement; + private readonly canvas: SVGElement; + + private readonly size: number; + private readonly border: number; + private readonly thickness: number; + private readonly color: string; + private readonly textColor: string; + + /** + * @internal + */ + constructor(viewer: Viewer) { + super(viewer, { className: 'psv-loader-container' }); + + this.loader = document.createElement('div'); + this.loader.className = 'psv-loader'; + this.container.appendChild(this.loader); + + this.size = this.loader.offsetWidth; + + this.canvas = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + this.canvas.setAttribute('class', 'psv-loader-canvas'); + this.canvas.setAttribute('viewBox', `0 0 ${this.size} ${this.size}`); + this.loader.appendChild(this.canvas); + + this.textColor = getStyle(this.loader, 'color'); + this.color = getStyle(this.canvas, 'color'); + this.border = parseInt(getStyle(this.loader, 'outlineWidth'), 10); + this.thickness = parseInt(getStyle(this.canvas, 'outlineWidth'), 10); + + this.viewer.addEventListener(ConfigChangedEvent.type, this); + + this.__updateContent(); + this.hide(); + } + + /** + * @internal + */ + override destroy(): void { + this.viewer.removeEventListener(ConfigChangedEvent.type, this); + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof ConfigChangedEvent) { + e.containsOptions('loadingImg', 'loadingTxt') && this.__updateContent(); + } + } + + /** + * Sets the loader progression + */ + setProgress(value: number) { + const angle = Math.min(value, 99.999) / 100 * Math.PI * 2; + const halfSize = this.size / 2; + const startX = halfSize; + const startY = this.thickness / 2 + this.border; + const radius = (this.size - this.thickness) / 2 - this.border; + const endX = Math.sin(angle) * radius + halfSize; + const endY = -Math.cos(angle) * radius + halfSize; + const largeArc = value > 50 ? '1' : '0'; + + this.canvas.innerHTML = ` + + + `; + + this.viewer.dispatchEvent(new LoadProgressEvent(Math.round(value))); + } + + private __updateContent() { + const current = this.loader.querySelector('.psv-loader-image, .psv-loader-text'); + if (current) { + this.loader.removeChild(current); + } + + let inner; + if (this.viewer.config.loadingImg) { + inner = document.createElement('img'); + inner.className = 'psv-loader-image'; + inner.src = this.viewer.config.loadingImg; + } else if (this.viewer.config.loadingTxt) { + inner = document.createElement('div'); + inner.className = 'psv-loader-text'; + inner.innerHTML = this.viewer.config.loadingTxt; + } + if (inner) { + const size = Math.round( + Math.sqrt(2 * Math.pow((this.size / 2 - this.thickness / 2 - this.border), 2)) + ); + inner.style.maxWidth = size + 'px'; + inner.style.maxHeight = size + 'px'; + this.loader.appendChild(inner); + } + } +} diff --git a/packages/core/src/components/Navbar.ts b/packages/core/src/components/Navbar.ts new file mode 100644 index 000000000..4d7dafab8 --- /dev/null +++ b/packages/core/src/components/Navbar.ts @@ -0,0 +1,234 @@ +import { AbstractButton, ButtonConstructor } from '../buttons/AbstractButton'; +import { CustomButton } from '../buttons/CustomButton'; +import { DescriptionButton } from '../buttons/DescriptionButton'; +import { DownloadButton } from '../buttons/DownloadButton'; +import { FullscreenButton } from '../buttons/FullscreenButton'; +import { MenuButton } from '../buttons/MenuButton'; +import { MoveDownButton } from '../buttons/MoveDownButton'; +import { MoveLeftButton } from '../buttons/MoveLeftButton'; +import { MoveRightButton } from '../buttons/MoveRightButton'; +import { MoveUpButton } from '../buttons/MoveUpButton'; +import { ZoomInButton } from '../buttons/ZoomInButton'; +import { ZoomOutButton } from '../buttons/ZoomOutButton'; +import { ZoomRangeButton } from '../buttons/ZoomRangeButton'; +import { DEFAULTS } from '../data/config'; +import { ParsedViewerConfig } from '../model'; +import { PSVError } from '../PSVError'; +import { logWarn } from '../utils'; +import type { Viewer } from '../Viewer'; +import { AbstractComponent } from './AbstractComponent'; +import { NavbarCaption } from './NavbarCaption'; + +/** + * List of available buttons + */ +const AVAILABLE_BUTTONS: Record = {}; + +/** + * List of available buttons + */ +const AVAILABLE_GROUPS: Record = {}; + +/** + * Register a new button available for all viewers + * @param button + * @param [defaultPosition] If provided the default configuration of the navbar will be modified. + * Possible values are : + * - `start` + * - `end` + * - `[id]:left` + * - `[id]:right` + * @throws {@link PSVError} if the button constructor has no "id" + */ +export function registerButton(button: ButtonConstructor, defaultPosition?: string) { + if (!button.id) { + throw new PSVError('Button id is required'); + } + + AVAILABLE_BUTTONS[button.id] = button; + + if (button.groupId) { + (AVAILABLE_GROUPS[button.groupId] = AVAILABLE_GROUPS[button.groupId] || []).push(button); + } + + if (defaultPosition) { + const navbar = DEFAULTS.navbar as string[]; + switch (defaultPosition) { + case 'start': + navbar.unshift(button.id); + break; + case 'end': + navbar.push(button.id); + break; + default: { + const [id, pos] = defaultPosition.split(':'); + const idx = navbar.indexOf(id); + if (!id || !pos || idx === -1) { + throw new PSVError(`Invalid defaultPosition ${defaultPosition}`); + } + navbar.splice(idx + (pos === 'right' ? 1 : 0), 0, button.id); + } + } + } +} + +[ + ZoomOutButton, + ZoomRangeButton, + ZoomInButton, + DescriptionButton, + NavbarCaption, + DownloadButton, + FullscreenButton, + MoveLeftButton, + MoveRightButton, + MoveUpButton, + MoveDownButton, +].forEach((btn) => registerButton(btn)); + +/** + * Navigation bar component + */ +export class Navbar extends AbstractComponent { + /** + * @internal + */ + collapsed: AbstractButton[] = []; + + /** + * @internal + */ + constructor(viewer: Viewer) { + super(viewer, { + className: 'psv-navbar psv--capture-event', + }); + + this.state.visible = false; + } + + /** + * Shows the navbar + */ + override show() { + this.container.classList.add('psv-navbar--open'); + this.state.visible = true; + } + + /** + * Hides the navbar + */ + override hide() { + this.container.classList.remove('psv-navbar--open'); + this.state.visible = false; + } + + /** + * Change the buttons visible on the navbar + */ + setButtons(buttons: ParsedViewerConfig['navbar']) { + this.children.slice().forEach((item) => item.destroy()); + this.children.length = 0; + + // force description button if caption is present (used on narrow screens) + if (buttons.indexOf(NavbarCaption.id) !== -1 && buttons.indexOf(DescriptionButton.id) === -1) { + buttons.splice(buttons.indexOf(NavbarCaption.id), 0, DescriptionButton.id); + } + + buttons.forEach((button) => { + if (typeof button === 'object') { + new CustomButton(this, button); + } else if (AVAILABLE_BUTTONS[button]) { + // @ts-ignore + new AVAILABLE_BUTTONS[button](this); + } else if (AVAILABLE_GROUPS[button]) { + AVAILABLE_GROUPS[button].forEach((buttonCtor) => { + // @ts-ignore + new buttonCtor(this); + }); + } else { + logWarn(`Unknown button ${button}`); + } + }); + + new MenuButton(this); + + this.children.forEach((item) => { + if (item instanceof AbstractButton) { + item.checkSupported(); + } + }); + + this.autoSize(); + } + + /** + * Changes the navbar caption + */ + setCaption(html: string) { + this.children.some((item) => { + if (item instanceof NavbarCaption) { + item.setCaption(html); + return true; + } + }); + } + + /** + * Returns a button by its identifier + */ + getButton(id: string, warnNotFound = true): AbstractButton { + const button = this.children.find((item) => { + return item instanceof AbstractButton && item.id === id; + }); + + if (!button && warnNotFound) { + logWarn(`button "${id}" not found in the navbar`); + } + + return button as AbstractButton; + } + + /** + * Automatically collapses buttons + * @internal + */ + autoSize() { + this.children.forEach((child) => { + if (child instanceof AbstractButton) { + child.autoSize(); + } + }); + + const availableWidth = this.container.offsetWidth; + + let totalWidth = 0; + const collapsableButtons: AbstractButton[] = []; + + this.children.forEach((item) => { + if (item.isVisible() && item instanceof AbstractButton) { + totalWidth += item.width; + if (item.collapsable) { + collapsableButtons.push(item); + } + } + }); + + if (totalWidth === 0) { + return; + } + + if (availableWidth < totalWidth && collapsableButtons.length > 0) { + collapsableButtons.forEach((item) => item.collapse()); + this.collapsed = collapsableButtons; + + this.getButton(MenuButton.id).show(false); + } else if (availableWidth >= totalWidth && this.collapsed.length > 0) { + this.collapsed.forEach((item) => item.uncollapse()); + this.collapsed = []; + + this.getButton(MenuButton.id).hide(false); + } + + this.getButton(NavbarCaption.id, false)?.autoSize(); + } +} diff --git a/packages/core/src/components/NavbarCaption.ts b/packages/core/src/components/NavbarCaption.ts new file mode 100644 index 000000000..1a11f7b0c --- /dev/null +++ b/packages/core/src/components/NavbarCaption.ts @@ -0,0 +1,70 @@ +import { AbstractButton } from '../buttons/AbstractButton'; +import { DescriptionButton } from '../buttons/DescriptionButton'; +import type { Navbar } from './Navbar'; + +export class NavbarCaption extends AbstractButton { + static override readonly id = 'caption'; + + private contentWidth = 0; + + private readonly contentElt: HTMLElement; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-caption', + hoverScale: false, + collapsable: false, + tabbable: true, + }); + + this.state.width = 0; + + this.contentElt = document.createElement('div'); + this.contentElt.className = 'psv-caption-content'; + this.container.appendChild(this.contentElt); + + this.setCaption(this.viewer.config.caption); + } + + override hide() { + this.contentElt.style.display = 'none'; + this.state.visible = false; + } + + override show() { + this.contentElt.style.display = ''; + this.state.visible = true; + } + + onClick(): void { + // nothing + } + + /** + * Changes the caption + */ + setCaption(html: string) { + this.show(); + this.contentElt.innerHTML = html ?? ''; + + if (this.contentElt.innerHTML) { + this.contentWidth = this.contentElt.offsetWidth; + } else { + this.contentWidth = 0; + } + + this.autoSize(); + } + + /** + * Toggles content and icon depending on available space + */ + override autoSize() { + this.toggle(this.container.offsetWidth >= this.contentWidth); + this.__refreshButton(); + } + + private __refreshButton() { + (this.viewer.navbar.getButton(DescriptionButton.id, false) as DescriptionButton)?.autoSize(true); + } +} diff --git a/packages/core/src/components/Notification.ts b/packages/core/src/components/Notification.ts new file mode 100644 index 000000000..a54da9e73 --- /dev/null +++ b/packages/core/src/components/Notification.ts @@ -0,0 +1,115 @@ +import { PSVError } from '../PSVError'; +import type { Viewer } from '../Viewer'; +import { HideNotificationEvent, ShowNotificationEvent } from '../events'; +import { AbstractComponent } from './AbstractComponent'; + +/** + * Configuration for {@link Notification.show} + */ +export type NotificationConfig = { + /** + * unique identifier to use with {@link Notification.hide} and {@link Notification.isVisible} + */ + id?: string; + /** + * notification content + */ + content: string; + /** + * automatically hide the notification after X milliseconds + */ + timeout?: number; +}; + +/** + * Notification component + */ +export class Notification extends AbstractComponent { + /** + * @internal + */ + protected override readonly state = { + visible: false, + contentId: null as string, + timeout: null as ReturnType, + }; + + private readonly content: HTMLElement; + + /** + * @internal + */ + constructor(viewer: Viewer) { + super(viewer, { + className: 'psv-notification', + }); + + this.content = document.createElement('div'); + this.content.className = 'psv-notification-content'; + this.container.appendChild(this.content); + + this.content.addEventListener('click', () => this.hide()); + } + + /** + * Checks if the notification is visible + */ + override isVisible(id?: string) { + return this.state.visible && (!id || !this.state.contentId || this.state.contentId === id); + } + + /** + * @throws {@link PSVError} always + * @internal + */ + override toggle() { + throw new PSVError('Notification cannot be toggled'); + } + + /** + * Displays a notification on the viewer + * + * @example + * viewer.showNotification({ content: 'Hello world', timeout: 5000 }) + * @example + * viewer.showNotification('Hello world') + */ + override show(config: string | NotificationConfig) { + if (this.state.timeout) { + clearTimeout(this.state.timeout); + this.state.timeout = null; + } + + if (typeof config === 'string') { + config = { content: config }; + } + + this.state.contentId = config.id || null; + this.content.innerHTML = config.content; + + this.container.classList.add('psv-notification--visible'); + this.state.visible = true; + + this.viewer.dispatchEvent(new ShowNotificationEvent(config.id)); + + if (config.timeout) { + this.state.timeout = setTimeout(() => this.hide(this.state.contentId), config.timeout); + } + } + + /** + * Hides the notification + */ + override hide(id?: string) { + if (this.isVisible(id)) { + const contentId = this.state.contentId; + + this.container.classList.remove('psv-notification--visible'); + this.state.visible = false; + + this.state.contentId = null; + + this.viewer.dispatchEvent(new HideNotificationEvent(contentId)); + } + } +} diff --git a/packages/core/src/components/Overlay.ts b/packages/core/src/components/Overlay.ts new file mode 100644 index 000000000..d8c1b233c --- /dev/null +++ b/packages/core/src/components/Overlay.ts @@ -0,0 +1,152 @@ +import { KEY_CODES } from '../data/constants'; +import { PSVError } from '../PSVError'; +import type { Viewer } from '../Viewer'; +import { ClickEvent, HideOverlayEvent, KeypressEvent, ShowOverlayEvent } from '../events'; +import { AbstractComponent } from './AbstractComponent'; + +/** + * Configuration for {@link Overlay.show} + */ +export type OverlayConfig = { + /** + * unique identifier to use with {@link Overlay.hide} and {@link Overlay.isVisible} + */ + id?: string; + /** + * SVG image/icon displayed above the text + */ + image?: string; + /** + * main message + */ + title: string; + /** + * secondary message + */ + text?: string; + /** + * if the user can hide the overlay by clicking + * @default true + */ + dissmisable?: boolean; +}; + +/** + * Overlay component + */ +export class Overlay extends AbstractComponent { + /** + * @internal + */ + protected override readonly state = { + visible: false, + contentId: null as string, + dissmisable: true, + }; + + private readonly image: HTMLElement; + private readonly title: HTMLElement; + private readonly text: HTMLElement; + + /** + * @internal + */ + constructor(viewer: Viewer) { + super(viewer, { + className: 'psv-overlay', + }); + + this.image = document.createElement('div'); + this.image.className = 'psv-overlay-image'; + this.container.appendChild(this.image); + + this.title = document.createElement('div'); + this.title.className = 'psv-overlay-title'; + this.container.appendChild(this.title); + + this.text = document.createElement('div'); + this.text.className = 'psv-overlay-text'; + this.container.appendChild(this.text); + + this.viewer.addEventListener(ClickEvent.type, this); + this.viewer.addEventListener(KeypressEvent.type, this); + + super.hide(); + } + + /** + * @internal + */ + override destroy() { + this.viewer.removeEventListener(ClickEvent.type, this); + this.viewer.removeEventListener(KeypressEvent.type, this); + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof ClickEvent) { + if (this.isVisible() && this.state.dissmisable) { + this.hide(); + e.stopPropagation(); + } + } else if (e instanceof KeypressEvent) { + if (this.isVisible() && this.state.dissmisable && e.key === KEY_CODES.Escape) { + this.hide(); + e.preventDefault(); + } + } + } + + /** + * Checks if the overlay is visible + */ + override isVisible(id?: string) { + return this.state.visible && (!id || !this.state.contentId || this.state.contentId === id); + } + + /** + * @throws {@link PSVError} always + * @internal + */ + override toggle() { + throw new PSVError('Overlay cannot be toggled'); + } + + /** + * Displays an overlay on the viewer + */ + override show(config: string | OverlayConfig) { + if (typeof config === 'string') { + config = { title: config }; + } + + this.state.contentId = config.id || null; + this.state.dissmisable = config.dissmisable !== false; + this.image.innerHTML = config.image || ''; + this.title.innerHTML = config.title || ''; + this.text.innerHTML = config.text || ''; + + super.show(); + + this.viewer.dispatchEvent(new ShowOverlayEvent(config.id)); + } + + /** + * Hides the overlay + */ + override hide(id?: string) { + if (this.isVisible(id)) { + const contentId = this.state.contentId; + + super.hide(); + + this.state.contentId = null; + + this.viewer.dispatchEvent(new HideOverlayEvent(contentId)); + } + } +} diff --git a/packages/core/src/components/Panel.ts b/packages/core/src/components/Panel.ts new file mode 100644 index 000000000..811c6acb2 --- /dev/null +++ b/packages/core/src/components/Panel.ts @@ -0,0 +1,291 @@ +import { KEY_CODES } from '../data/constants'; +import { PSVError } from '../PSVError'; +import { toggleClass } from '../utils'; +import type { Viewer } from '../Viewer'; +import { HidePanelEvent, KeypressEvent, ShowPanelEvent } from '../events'; +import { AbstractComponent } from './AbstractComponent'; + +const PANEL_MIN_WIDTH = 200; + +const PANEL_CLASS_NO_INTERACTION = 'psv-panel-content--no-interaction'; + +/** + * Configuration for {@link Panel.show} + */ +export type PanelConfig = { + /** + * unique identifier to use with {@link Panel.hide} and {@link Panel.isVisible} and to store the width + */ + id?: string; + /** + * HTML content of the panel + */ + content: string; + /** + * remove the default margins + * @default false + */ + noMargin?: boolean; + /** + * initial width + */ + width?: string; + /** + * called when the user clicks inside the panel or presses the Enter key while an element focused + */ + clickHandler?: (target: HTMLElement) => void; +}; + +/** + * Panel component + */ +export class Panel extends AbstractComponent { + /** + * @internal + */ + protected override readonly state = { + visible: false, + contentId: null as string, + mouseX: 0, + mouseY: 0, + mousedown: false, + clickHandler: null as (e: MouseEvent) => void, + keyHandler: null as (e: KeyboardEvent) => void, + width: {} as Record, + }; + + private readonly content: HTMLElement; + + /** + * @internal + */ + constructor(viewer: Viewer) { + super(viewer, { + className: 'psv-panel psv--capture-event', + }); + + const resizer = document.createElement('div'); + resizer.className = 'psv-panel-resizer'; + this.container.appendChild(resizer); + + const closeBtn = document.createElement('div'); + closeBtn.className = 'psv-panel-close-button'; + this.container.appendChild(closeBtn); + + this.content = document.createElement('div'); + this.content.className = 'psv-panel-content'; + this.container.appendChild(this.content); + + // Stop wheel event bubbling from panel + this.container.addEventListener('wheel', (e) => e.stopPropagation()); + + closeBtn.addEventListener('click', () => this.hide()); + + // Event for panel resizing + stop bubling + resizer.addEventListener('mousedown', this); + resizer.addEventListener('touchstart', this); + this.viewer.container.addEventListener('mouseup', this); + this.viewer.container.addEventListener('touchend', this); + this.viewer.container.addEventListener('mousemove', this); + this.viewer.container.addEventListener('touchmove', this); + + this.viewer.addEventListener(KeypressEvent.type, this); + } + + /** + * @internal + */ + override destroy() { + this.viewer.removeEventListener(KeypressEvent.type, this); + + this.viewer.container.removeEventListener('mousemove', this); + this.viewer.container.removeEventListener('touchmove', this); + this.viewer.container.removeEventListener('mouseup', this); + this.viewer.container.removeEventListener('touchend', this); + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + // prettier-ignore + switch (e.type) { + case 'mousedown': this.__onMouseDown(e as MouseEvent); break; + case 'touchstart': this.__onTouchStart(e as TouchEvent); break; + case 'mousemove': this.__onMouseMove(e as MouseEvent); break; + case 'touchmove': this.__onTouchMove(e as TouchEvent); break; + case 'mouseup': this.__onMouseUp(e as MouseEvent); break; + case 'touchend': this.__onTouchEnd(e as TouchEvent); break; + case KeypressEvent.type: this.__onKeyPress(e as KeypressEvent); break; + } + } + + /** + * Checks if the panel is visible + */ + override isVisible(id?: string) { + return this.state.visible && (!id || !this.state.contentId || this.state.contentId === id); + } + + /** + * @throws {@link PSVError} always + * @internal + */ + override toggle() { + throw new PSVError('Panel cannot be toggled'); + } + + /** + * Shows the panel + */ + override show(config: string | PanelConfig) { + if (typeof config === 'string') { + config = { content: config }; + } + const wasVisible = this.isVisible(config.id); + + this.state.contentId = config.id || null; + this.state.visible = true; + + if (this.state.clickHandler) { + this.content.removeEventListener('click', this.state.clickHandler); + this.content.removeEventListener('keydown', this.state.keyHandler); + this.state.clickHandler = null; + this.state.keyHandler = null; + } + + if (config.id && this.state.width[config.id]) { + this.container.style.width = this.state.width[config.id]; + } else if (config.width) { + this.container.style.width = config.width; + } else { + this.container.style.width = null; + } + + this.content.innerHTML = config.content; + this.content.scrollTop = 0; + this.container.classList.add('psv-panel--open'); + + toggleClass(this.content, 'psv-panel-content--no-margin', config.noMargin === true); + + if (config.clickHandler) { + this.state.clickHandler = (e) => { + (config as PanelConfig).clickHandler(e.target as HTMLElement); + }; + this.state.keyHandler = (e) => { + if (e.key === KEY_CODES.Enter) { + (config as PanelConfig).clickHandler(e.target as HTMLElement); + } + }; + this.content.addEventListener('click', this.state.clickHandler); + this.content.addEventListener('keydown', this.state.keyHandler); + + // focus the first element if possible, after animation ends + if (!wasVisible) { + setTimeout(() => { + (this.content.querySelector('a,button,[tabindex]') as HTMLElement)?.focus(); + }, 300); + } + } + + this.viewer.dispatchEvent(new ShowPanelEvent(config.id)); + } + + /** + * Hides the panel + */ + override hide(id?: string) { + if (this.isVisible(id)) { + const contentId = this.state.contentId; + + this.state.visible = false; + this.state.contentId = null; + + this.content.innerHTML = null; + this.container.classList.remove('psv-panel--open'); + + if (this.state.clickHandler) { + this.content.removeEventListener('click', this.state.clickHandler); + this.state.clickHandler = null; + } + + this.viewer.dispatchEvent(new HidePanelEvent(contentId)); + } + } + + private __onMouseDown(evt: MouseEvent) { + evt.stopPropagation(); + this.__startResize(evt.clientX, evt.clientY); + } + + private __onTouchStart(evt: TouchEvent) { + evt.stopPropagation(); + if (evt.touches.length === 1) { + const touch = evt.touches[0]; + this.__startResize(touch.clientX, touch.clientY); + } + } + + private __onMouseUp(evt: MouseEvent) { + if (this.state.mousedown) { + evt.stopPropagation(); + this.state.mousedown = false; + this.content.classList.remove(PANEL_CLASS_NO_INTERACTION); + } + } + + private __onTouchEnd(evt: TouchEvent) { + if (this.state.mousedown) { + evt.stopPropagation(); + if (evt.touches.length === 0) { + this.state.mousedown = false; + this.content.classList.remove(PANEL_CLASS_NO_INTERACTION); + } + } + } + + private __onMouseMove(evt: MouseEvent) { + if (this.state.mousedown) { + evt.stopPropagation(); + this.__resize(evt.clientX, evt.clientY); + } + } + + private __onTouchMove(evt: TouchEvent) { + if (this.state.mousedown) { + const touch = evt.touches[0]; + this.__resize(touch.clientX, touch.clientY); + } + } + + private __onKeyPress(evt: KeypressEvent) { + if (this.isVisible() && evt.key === KEY_CODES.Escape) { + this.hide(); + evt.preventDefault(); + } + } + + private __startResize(clientX: number, clientY: number) { + this.state.mouseX = clientX; + this.state.mouseY = clientY; + this.state.mousedown = true; + this.content.classList.add(PANEL_CLASS_NO_INTERACTION); + } + + private __resize(clientX: number, clientY: number) { + const x = clientX; + const y = clientY; + const width = Math.max(PANEL_MIN_WIDTH, this.container.offsetWidth - (x - this.state.mouseX)) + 'px'; + + if (this.state.contentId) { + this.state.width[this.state.contentId] = width; + } + + this.container.style.width = width; + + this.state.mouseX = x; + this.state.mouseY = y; + } +} diff --git a/packages/core/src/components/Tooltip.ts b/packages/core/src/components/Tooltip.ts new file mode 100644 index 000000000..75f5d6218 --- /dev/null +++ b/packages/core/src/components/Tooltip.ts @@ -0,0 +1,404 @@ +import { PSVError } from '../PSVError'; +import { addClasses, cleanCssPosition, getStyle, cssPositionIsOrdered } from '../utils'; +import type { Viewer } from '../Viewer'; +import { HideTooltipEvent, ShowTooltipEvent } from '../events'; +import { AbstractComponent } from './AbstractComponent'; + +/** + * Object defining the tooltip position + */ +export type TooltipPosition = { + /** + * Position of the tip of the arrow of the tooltip, in pixels + */ + top: number; + /** + * Position of the tip of the arrow of the tooltip, in pixels + */ + left: number; + /** + * Tooltip position toward it's arrow tip. + * Accepted values are combinations of `top`, `center`, `bottom` and `left`, `center`, `right`. + */ + position?: string | [string, string]; + /** + * @internal + */ + box?: { width: number; height: number }; +}; + +/** + * Configuration for {@link Viewer.createTooltip} + */ +export type TooltipConfig = TooltipPosition & { + /** + * HTML content of the tooltip + */ + content: string; + /** + * Additional CSS class added to the tooltip + */ + className?: string; + /** + * Userdata associated to the tooltip + */ + data?: any; +}; + +type TooltipStyle = { + posClass: [string, string]; + width: number; + height: number; + top: number; + left: number; + arrowTop: number; + arrowLeft: number; +}; + +const enum TooltipState { + NONE, + SHOWING, + HIDING, + READY, +} + +/** + * Tooltip component + * @description Never instanciate tooltips directly use {@link Viewer#createTooltip} instead + */ +export class Tooltip extends AbstractComponent { + /** + * @internal + */ + protected override readonly state = { + visible: true, + arrow: 0, + border: 0, + state: TooltipState.NONE, + width: 0, + height: 0, + pos: '', + config: null as TooltipPosition, + data: null as any, + }; + + private readonly content: HTMLElement; + private readonly arrow: HTMLElement; + + /** + * @internal + */ + constructor(viewer: Viewer, config: TooltipConfig) { + super(viewer, { + className: 'psv-tooltip', + }); + + this.content = document.createElement('div'); + this.content.className = 'psv-tooltip-content'; + this.container.appendChild(this.content); + + this.arrow = document.createElement('div'); + this.arrow.className = 'psv-tooltip-arrow'; + this.container.appendChild(this.arrow); + + this.container.addEventListener('transitionend', this); + + this.container.style.top = '-1000px'; + this.container.style.left = '-1000px'; + + this.show(config); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e.type === 'transitionend') { + this.__onTransitionEnd(e as TransitionEvent); + } + } + + /** + * @internal + */ + override destroy() { + delete this.state.data; + super.destroy(); + } + + /** + * @throws {@link PSVError} always + * @internal + */ + override toggle() { + throw new PSVError('Tooltip cannot be toggled'); + } + + /** + * Displays the tooltip on the viewer + * @internal + */ + override show(config: TooltipConfig) { + if (this.state.state !== TooltipState.NONE) { + throw new PSVError('Initialized tooltip cannot be re-initialized'); + } + + if (config.className) { + addClasses(this.container, config.className); + } + + this.state.state = TooltipState.READY; + + this.update(config.content, config); + + this.state.data = config.data; + this.state.state = TooltipState.SHOWING; + + this.viewer.dispatchEvent(new ShowTooltipEvent(this, this.state.data)); + + this.__waitImages(); + } + + /** + * Updates the content of the tooltip, optionally with a new position + * @throws {@link PSVError} if the configuration is invalid + */ + update(content: string, config?: TooltipPosition) { + this.content.innerHTML = content; + + const rect = this.container.getBoundingClientRect(); + this.state.width = rect.right - rect.left; + this.state.height = rect.bottom - rect.top; + this.state.arrow = parseInt(getStyle(this.arrow, 'borderTopWidth'), 10); + this.state.border = parseInt(getStyle(this.container, 'borderTopLeftRadius'), 10); + + this.move(config ?? this.state.config); + } + + /** + * Moves the tooltip to a new position + * @throws {@link PSVError} if the configuration is invalid + */ + move(config: TooltipPosition) { + if (this.state.state !== TooltipState.SHOWING && this.state.state !== TooltipState.READY) { + throw new PSVError('Uninitialized tooltip cannot be moved'); + } + + if (!config.box) { + config.box = { + width: 0, + height: 0, + }; + } + + this.state.config = config; + + const t = this.container; + const a = this.arrow; + + // compute size + const style: TooltipStyle = { + posClass: cleanCssPosition(config.position, { allowCenter: false, cssOrder: false }) || ['top', 'center'], + width: this.state.width, + height: this.state.height, + top: 0, + left: 0, + arrowTop: 0, + arrowLeft: 0, + }; + + // set initial position + this.__computeTooltipPosition(style, config); + + // correct position if overflow + let swapY = null; + let swapX = null; + if (style.top < 0) { + swapY = 'bottom'; + } else if (style.top + style.height > this.viewer.state.size.height) { + swapY = 'top'; + } + if (style.left < 0) { + swapX = 'right'; + } else if (style.left + style.width > this.viewer.state.size.width) { + swapX = 'left'; + } + if (swapX || swapY) { + const ordered = cssPositionIsOrdered(style.posClass); + if (swapY) { + style.posClass[ordered ? 0 : 1] = swapY; + } + if (swapX) { + style.posClass[ordered ? 1 : 0] = swapX; + } + this.__computeTooltipPosition(style, config); + } + + // apply position + t.style.top = style.top + 'px'; + t.style.left = style.left + 'px'; + + a.style.top = style.arrowTop + 'px'; + a.style.left = style.arrowLeft + 'px'; + + const newPos = style.posClass.join('-'); + if (newPos !== this.state.pos) { + t.classList.remove(`psv-tooltip--${this.state.pos}`); + + this.state.pos = newPos; + t.classList.add(`psv-tooltip--${this.state.pos}`); + } + } + + /** + * Hides the tooltip + */ + override hide() { + this.container.classList.remove('psv-tooltip--visible'); + this.state.state = TooltipState.HIDING; + + this.viewer.dispatchEvent(new HideTooltipEvent(this.state.data)); + } + + /** + * Finalize transition + */ + private __onTransitionEnd(e: TransitionEvent) { + if (e.propertyName === 'transform') { + switch (this.state.state) { + case TooltipState.SHOWING: + this.container.classList.add('psv-tooltip--visible'); + this.state.state = TooltipState.READY; + break; + + case TooltipState.HIDING: + this.state.state = TooltipState.NONE; + this.destroy(); + break; + + default: + // nothing + } + } + } + + /** + * Computes the position of the tooltip and its arrow + */ + private __computeTooltipPosition(style: TooltipStyle, config: TooltipPosition) { + const arrow = this.state.arrow; + const top = config.top; + const height = style.height; + const left = config.left; + const width = style.width; + const offsetSide = arrow + this.state.border; + const offsetX = config.box.width / 2 + arrow * 2; + const offsetY = config.box.height / 2 + arrow * 2; + + switch (style.posClass.join('-')) { + case 'top-left': + style.top = top - offsetY - height; + style.left = left + offsetSide - width; + style.arrowTop = height; + style.arrowLeft = width - offsetSide - arrow; + break; + case 'top-center': + style.top = top - offsetY - height; + style.left = left - width / 2; + style.arrowTop = height; + style.arrowLeft = width / 2 - arrow; + break; + case 'top-right': + style.top = top - offsetY - height; + style.left = left - offsetSide; + style.arrowTop = height; + style.arrowLeft = arrow; + break; + case 'bottom-left': + style.top = top + offsetY; + style.left = left + offsetSide - width; + style.arrowTop = -arrow * 2; + style.arrowLeft = width - offsetSide - arrow; + break; + case 'bottom-center': + style.top = top + offsetY; + style.left = left - width / 2; + style.arrowTop = -arrow * 2; + style.arrowLeft = width / 2 - arrow; + break; + case 'bottom-right': + style.top = top + offsetY; + style.left = left - offsetSide; + style.arrowTop = -arrow * 2; + style.arrowLeft = arrow; + break; + case 'left-top': + style.top = top + offsetSide - height; + style.left = left - offsetX - width; + style.arrowTop = height - offsetSide - arrow; + style.arrowLeft = width; + break; + case 'center-left': + style.top = top - height / 2; + style.left = left - offsetX - width; + style.arrowTop = height / 2 - arrow; + style.arrowLeft = width; + break; + case 'left-bottom': + style.top = top - offsetSide; + style.left = left - offsetX - width; + style.arrowTop = arrow; + style.arrowLeft = width; + break; + case 'right-top': + style.top = top + offsetSide - height; + style.left = left + offsetX; + style.arrowTop = height - offsetSide - arrow; + style.arrowLeft = -arrow * 2; + break; + case 'center-right': + style.top = top - height / 2; + style.left = left + offsetX; + style.arrowTop = height / 2 - arrow; + style.arrowLeft = -arrow * 2; + break; + case 'right-bottom': + style.top = top - offsetSide; + style.left = left + offsetX; + style.arrowTop = arrow; + style.arrowLeft = -arrow * 2; + break; + + // no default + } + } + + /** + * If the tooltip contains images, recompute its size once they are loaded + */ + private __waitImages() { + const images = this.content.querySelectorAll('img'); + + if (images.length > 0) { + const promises: Promise[] = []; + + images.forEach((image) => { + promises.push( + new Promise((resolve) => { + image.onload = resolve; + image.onerror = resolve; + }) + ); + }); + + Promise.all(promises).then(() => { + if (this.state.state === TooltipState.SHOWING || this.state.state === TooltipState.READY) { + const rect = this.container.getBoundingClientRect(); + this.state.width = rect.right - rect.left; + this.state.height = rect.bottom - rect.top; + this.move(this.state.config); + } + }); + } + } +} diff --git a/packages/core/src/data/config.ts b/packages/core/src/data/config.ts new file mode 100644 index 000000000..e3adcb189 --- /dev/null +++ b/packages/core/src/data/config.ts @@ -0,0 +1,243 @@ +import { MathUtils } from 'three'; +import { adapterInterop } from '../adapters/AbstractAdapter'; +import { EquirectangularAdapter } from '../adapters/EquirectangularAdapter'; +import { ParsedViewerConfig, ReadonlyViewerConfig, ViewerConfig } from '../model'; +import { pluginInterop } from '../plugins/AbstractPlugin'; +import { PSVError } from '../PSVError'; +import { clone, ConfigParsers, getConfigParser, isNil, logWarn, parseAngle } from '../utils'; +import { ACTIONS, KEY_CODES } from './constants'; + +/** + * Default options + */ +export const DEFAULTS: Required = { + panorama: null, + overlay: null, + overlayOpacity: 1, + container: null, + adapter: [EquirectangularAdapter as any, null], + plugins: [], + caption: null, + description: null, + downloadUrl: null, + loadingImg: null, + loadingTxt: 'Loading...', + size: null, + fisheye: 0, + minFov: 30, + maxFov: 90, + defaultZoomLvl: 50, + defaultYaw: 0, + defaultPitch: 0, + sphereCorrection: null, + moveSpeed: 1, + zoomSpeed: 1, + moveInertia: true, + mousewheel: true, + mousemove: true, + mousewheelCtrlKey: false, + touchmoveTwoFingers: false, + useXmpData: true, + panoData: null, + requestHeaders: null, + canvasBackground: '#000', + withCredentials: false, + // prettier-ignore + navbar: [ + 'zoom', + 'move', + 'download', + 'description', + 'caption', + 'fullscreen', + ], + lang: { + zoom: 'Zoom', + zoomOut: 'Zoom out', + zoomIn: 'Zoom in', + moveUp: 'Move up', + moveDown: 'Move down', + moveLeft: 'Move left', + moveRight: 'Move right', + download: 'Download', + fullscreen: 'Fullscreen', + menu: 'Menu', + twoFingers: 'Use two fingers to navigate', + ctrlZoom: 'Use ctrl + scroll to zoom the image', + loadError: "The panorama can't be loaded", + }, + keyboard: { + [KEY_CODES.ArrowUp]: ACTIONS.ROTATE_UP, + [KEY_CODES.ArrowDown]: ACTIONS.ROTATE_DOWN, + [KEY_CODES.ArrowRight]: ACTIONS.ROTATE_RIGHT, + [KEY_CODES.ArrowLeft]: ACTIONS.ROTATE_LEFT, + [KEY_CODES.PageUp]: ACTIONS.ZOOM_IN, + [KEY_CODES.PageDown]: ACTIONS.ZOOM_OUT, + [KEY_CODES.Plus]: ACTIONS.ZOOM_IN, + [KEY_CODES.Minus]: ACTIONS.ZOOM_OUT, + }, +}; + +/** + * List of unmodifiable options and their error messages + * @internal + */ +export const READONLY_OPTIONS: Record = { + panorama: 'Use setPanorama method to change the panorama', + panoData: 'Use setPanorama method to change the panorama', + overlay: 'Use setOverlay method to changer the overlay', + overlayOpacity: 'Use setOverlay method to changer the overlay', + container: 'Cannot change viewer container', + adapter: 'Cannot change adapter', + plugins: 'Cannot change plugins', +}; + +const autorotateOpt = (): void => { + logWarn('autorotate option is deprecated, use the "autorotate" plugin instead'); + return null; +}; + +/** + * Parsers/validators for each option + * @internal + */ +export const CONFIG_PARSERS: ConfigParsers = { + container: (container) => { + if (!container) { + throw new PSVError('No value given for container.'); + } + return container; + }, + adapter: (adapter, { defValue }) => { + if (!adapter) { + adapter = defValue; + } else if (Array.isArray(adapter)) { + adapter = [adapterInterop(adapter[0]), adapter[1]]; + } else { + adapter = [adapterInterop(adapter), null]; + } + if (!adapter[0]) { + throw new PSVError('An undefined value was given for adapter.'); + } + if (!adapter[0].id) { + throw new PSVError(`Adapter has no id.`); + } + return adapter; + }, + overlayOpacity: (overlayOpacity) => { + return MathUtils.clamp(overlayOpacity, 0, 1); + }, + defaultLong() { + logWarn('defaultLong is deprecated, use defaultYaw instead'); + return null; + }, + defaultLat() { + logWarn('defaultLat is deprecated, use defaultPitch instead'); + return null; + }, + defaultYaw: (defaultYaw, { rawConfig }) => { + // defaultYaw is between 0 and PI + return parseAngle(!isNil(rawConfig.defaultLong) ? rawConfig.defaultLong : defaultYaw); + }, + defaultPitch: (defaultPitch, { rawConfig }) => { + // defaultPitch is between -PI/2 and PI/2 + return parseAngle(!isNil(rawConfig.defaultLat) ? rawConfig.defaultLat : defaultPitch, true); + }, + defaultZoomLvl: (defaultZoomLvl) => { + return MathUtils.clamp(defaultZoomLvl, 0, 100); + }, + minFov: (minFov, { rawConfig }) => { + // minFov and maxFov must be ordered + if (rawConfig.maxFov < minFov) { + logWarn('maxFov cannot be lower than minFov'); + minFov = rawConfig.maxFov; + } + // minFov between 1 and 179 + return MathUtils.clamp(minFov, 1, 179); + }, + maxFov: (maxFov, { rawConfig }) => { + // minFov and maxFov must be ordered + if (maxFov < rawConfig.minFov) { + maxFov = rawConfig.minFov; + } + // maxFov between 1 and 179 + return MathUtils.clamp(maxFov, 1, 179); + }, + lang: (lang) => { + if (Array.isArray(lang.twoFingers)) { + logWarn('lang.twoFingers must not be an array'); + lang.twoFingers = lang.twoFingers[0]; + } + return { + ...DEFAULTS.lang, + ...lang, + }; + }, + keyboard: (keyboard) => { + if (!keyboard) { + return null; + } + if (keyboard === true) { + return clone(DEFAULTS.keyboard); + } + return keyboard; + }, + autorotateLat: autorotateOpt, + autorotateDelay: autorotateOpt, + autorotateZoomLvl: autorotateOpt, + autorotateSpeed: autorotateOpt, + autorotateIdle: autorotateOpt, + fisheye: (fisheye) => { + // translate boolean fisheye to amount + if (fisheye === true) { + return 1; + } else if (fisheye === false) { + return 0; + } + return fisheye; + }, + requestHeaders: (requestHeaders) => { + if (requestHeaders && typeof requestHeaders === 'object') { + return () => requestHeaders; + } + if (typeof requestHeaders === 'function') { + return requestHeaders; + } + return null; + }, + plugins: (plugins) => { + return plugins.map((plugin, i) => { + if (Array.isArray(plugin)) { + plugin = [pluginInterop(plugin[0]), plugin[1]]; + } else { + plugin = [pluginInterop(plugin), null]; + } + if (!plugin[0]) { + throw new PSVError(`An undefined value was given for plugin ${i}.`); + } + if (!plugin[0].id) { + throw new PSVError(`Plugin ${i} has no id.`); + } + return plugin; + }); + }, + navbar: (navbar) => { + if (navbar === false) { + return null; + } + if (navbar === true) { + // true becomes the default array + return clone(DEFAULTS.navbar as string[]); + } + if (typeof navbar === 'string') { + // can be a space or coma separated list + return navbar.split(/[ ,]/); + } + return navbar; + }, +}; + +/** + * @internal + */ +export const getViewerConfig = getConfigParser(DEFAULTS, CONFIG_PARSERS); diff --git a/packages/core/src/data/constants.ts b/packages/core/src/data/constants.ts new file mode 100644 index 000000000..5c3685ec0 --- /dev/null +++ b/packages/core/src/data/constants.ts @@ -0,0 +1,130 @@ +/** + * Default duration of the transition between panoramas + */ +export const DEFAULT_TRANSITION = 1500; + +/** + * Minimum duration of the animations created with {@link Viewer#animate} + */ +export const ANIMATION_MIN_DURATION = 500; + +/** + * Number of pixels bellow which a mouse move will be considered as a click + */ +export const MOVE_THRESHOLD = 4; + +/** + * Delay in milliseconds between two clicks to consider a double click + */ +export const DBLCLICK_DELAY = 300; + +/** + * Delay in milliseconds to emulate a long touch + */ +export const LONGTOUCH_DELAY = 500; + +/** + * Delay in milliseconds to for the two fingers overlay to appear + */ +export const TWOFINGERSOVERLAY_DELAY = 100; + +/** + * Duration in milliseconds of the "ctrl zoom" overlay + */ +export const CTRLZOOM_TIMEOUT = 2000; + +/** + * Duration of the mouse position history used to compute inertia + */ +export const INERTIA_WINDOW = 300; + +/** + * Radius of the SphereGeometry, Half-length of the BoxGeometry + */ +export const SPHERE_RADIUS = 10; + +/** + * Property name added to viewer element + */ +export const VIEWER_DATA = 'photoSphereViewer'; + +/** + * Actions available for {@link ViewerConfig['keyboard']} configuration + */ +export enum ACTIONS { + ROTATE_UP = 'ROTATE_UP', + ROTATE_DOWN = 'ROTATE_DOWN', + ROTATE_RIGHT = 'ROTATE_RIGHT', + ROTATE_LEFT = 'ROTATE_LEFT', + ZOOM_IN = 'ZOOM_IN', + ZOOM_OUT = 'ZOOM_OUT', +} + +/** + * Internal identifiers for various stuff + * @internal + */ +export const IDS = { + MENU: 'menu', + TWO_FINGERS: 'twoFingers', + CTRL_ZOOM: 'ctrlZoom', + ERROR: 'error', + DESCRIPTION: 'description', +}; + +/** + * Subset of keyboard codes + */ +export const KEY_CODES = { + Enter: 'Enter', + Control: 'Control', + Escape: 'Escape', + Space: ' ', + PageUp: 'PageUp', + PageDown: 'PageDown', + ArrowLeft: 'ArrowLeft', + ArrowUp: 'ArrowUp', + ArrowRight: 'ArrowRight', + ArrowDown: 'ArrowDown', + Delete: 'Delete', + Plus: '+', + Minus: '-', +}; + +// @formatter:off +/** + * Collection of easing functions + * @link https://gist.github.com/frederickk/6165768 + */ +export const EASINGS: Record number> = { + linear: (t: number) => t, + + inQuad: (t: number) => t * t, + outQuad: (t: number) => t * (2 - t), + inOutQuad: (t: number) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t), + + inCubic: (t: number) => t * t * t, + outCubic: (t: number) => --t * t * t + 1, + inOutCubic: (t: number) => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1), + + inQuart: (t: number) => t * t * t * t, + outQuart: (t: number) => 1 - --t * t * t * t, + inOutQuart: (t: number) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t), + + inQuint: (t: number) => t * t * t * t * t, + outQuint: (t: number) => 1 + --t * t * t * t * t, + inOutQuint: (t: number) => (t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t), + + inSine: (t: number) => 1 - Math.cos(t * (Math.PI / 2)), + outSine: (t: number) => Math.sin(t * (Math.PI / 2)), + inOutSine: (t: number) => 0.5 - 0.5 * Math.cos(Math.PI * t), + + inExpo: (t: number) => Math.pow(2, 10 * (t - 1)), + outExpo: (t: number) => 1 - Math.pow(2, -10 * t), + inOutExpo: (t: number) => ((t = t * 2 - 1) < 0 ? 0.5 * Math.pow(2, 10 * t) : 1 - 0.5 * Math.pow(2, -10 * t)), + + inCirc: (t: number) => 1 - Math.sqrt(1 - t * t), + outCirc: (t: number) => Math.sqrt(1 - (t - 1) * (t - 1)), + 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), +}; +// @formatter:on diff --git a/packages/core/src/data/system.ts b/packages/core/src/data/system.ts new file mode 100644 index 000000000..68903e251 --- /dev/null +++ b/packages/core/src/data/system.ts @@ -0,0 +1,172 @@ +import { VIEWER_DATA } from './constants'; +import { InitialPromise } from '../model'; +import { PSVError } from '../PSVError'; + +const LOCALSTORAGE_TOUCH_SUPPORT = `${VIEWER_DATA}_touchSupport`; + +/** + * General information about the system + */ +export const SYSTEM = { + /** + * Indicates if the system data has been loaded + */ + loaded: false, + + /** + * Device screen pixel ratio + */ + pixelRatio: 1, + + /** + * Device supports WebGL + */ + isWebGLSupported: false, + + /** + * Maximum WebGL texture width + */ + maxTextureWidth: 0, + + /** + * Device supports touch events + */ + isTouchEnabled: null as InitialPromise, + + /** + * @internal + */ + __maxCanvasWidth: null as number | null, + + /** + * Maximum canvas width + */ + get maxCanvasWidth(): number { + if (this.__maxCanvasWidth === null) { + this.__maxCanvasWidth = getMaxCanvasWidth(this.maxTextureWidth); + } + return this.__maxCanvasWidth; + }, + + /** + * Loads the system if not already loaded + * @internal + */ + load() { + if (!this.loaded) { + const ctx = getWebGLCtx(); + + this.pixelRatio = window.devicePixelRatio || 1; + this.isWebGLSupported = ctx !== null; + this.maxTextureWidth = ctx ? ctx.getParameter(ctx.MAX_TEXTURE_SIZE) : 0; + this.isTouchEnabled = isTouchEnabled(); + this.loaded = true; + } + + if (!SYSTEM.isWebGLSupported) { + throw new PSVError('WebGL is not supported.'); + } + if (SYSTEM.maxTextureWidth === 0) { + throw new PSVError('Unable to detect system capabilities'); + } + }, +}; + +/** + * Tries to return a canvas webgl context + */ +function getWebGLCtx(): WebGLRenderingContext | null { + const canvas = document.createElement('canvas'); + const names = ['webgl2', 'webgl', 'experimental-webgl', 'moz-webgl', 'webkit-3d']; + let context = null; + + if (!canvas.getContext) { + return null; + } + + if ( + names.some((name) => { + try { + context = canvas.getContext(name); + return context !== null; + } catch (e) { + return false; + } + }) + ) { + return context; + } else { + return null; + } +} + +/** + * Detects if the user is using a touch screen + */ +function isTouchEnabled(): InitialPromise { + let initial = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + if (LOCALSTORAGE_TOUCH_SUPPORT in localStorage) { + initial = localStorage[LOCALSTORAGE_TOUCH_SUPPORT] === 'true'; + } + + const promise = new Promise((resolve) => { + const clear = () => { + window.removeEventListener('mousedown', listenerMouse); + window.removeEventListener('touchstart', listenerTouch); + clearTimeout(listenerTimeoutId); + }; + + const listenerMouse = () => { + clear(); + localStorage[LOCALSTORAGE_TOUCH_SUPPORT] = false; + resolve(false); + }; + + const listenerTouch = () => { + clear(); + localStorage[LOCALSTORAGE_TOUCH_SUPPORT] = true; + resolve(true); + }; + + const listenerTimeout = () => { + clear(); + localStorage[LOCALSTORAGE_TOUCH_SUPPORT] = initial; + resolve(initial); + }; + + window.addEventListener('mousedown', listenerMouse, false); + window.addEventListener('touchstart', listenerTouch, false); + const listenerTimeoutId = setTimeout(listenerTimeout, 10000); + }); + + return { initial, promise }; +} + +/** + * Gets max canvas width supported by the browser. + * We only test powers of 2 and height = width / 2 because that's what we need to generate WebGL textures + */ +function getMaxCanvasWidth(maxWidth: number): number { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = maxWidth; + canvas.height = maxWidth / 2; + + while (canvas.width > 1024) { + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, 1, 1); + + try { + if (ctx.getImageData(0, 0, 1, 1).data[0] > 0) { + return canvas.width; + } + } catch (e) { + // continue + } + + canvas.width /= 2; + canvas.height /= 2; + } + + throw new PSVError('Unable to detect system capabilities'); +} diff --git a/packages/core/src/events.ts b/packages/core/src/events.ts new file mode 100644 index 000000000..b6bf8633b --- /dev/null +++ b/packages/core/src/events.ts @@ -0,0 +1,414 @@ +import { Mesh } from 'three'; +import { Tooltip, TooltipConfig } from './components/Tooltip'; +import { TypedEvent } from './lib/TypedEventTarget'; +import { ClickData, Point, Position, PositionCompat, Size, TextureData, ViewerConfig } from './model'; +import type { Viewer } from './Viewer'; + +/** + * Base class for all events dispatched by {@link Viewer} + */ +export abstract class ViewerEvent extends TypedEvent {} + +/** + * @event Triggered before an animation, can be cancelled + */ +export class BeforeAnimateEvent extends ViewerEvent { + static override readonly type = 'before-animate'; + + /** @internal */ + constructor( + /** target position, can be modified */ + public position?: Position, + /** target zoom level, can be modified */ + public zoomLevel?: number + ) { + super(BeforeAnimateEvent.type, true); + } +} + +/** + * @event Triggered before a render + */ +export class BeforeRenderEvent extends ViewerEvent { + static override readonly type = 'before-render'; + + /** @internal */ + constructor( + /** time provided by requestAnimationFrame */ + public readonly timestamp: number, + /** time elapsed since the previous frame */ + public readonly elapsed: number + ) { + super(BeforeRenderEvent.type); + } +} + +/** + * @event Triggered before a rotate operation, can be cancelled + */ +export class BeforeRotateEvent extends ViewerEvent { + static override readonly type = 'before-rotate'; + + /** @internal */ + constructor( + /** target position, can be modified */ + public position: Position + ) { + super(BeforeRotateEvent.type, true); + } +} + +/** + * @event Triggered when the user clicks on the viewer (everywhere excluding the navbar and the side panel) + */ +export class ClickEvent extends ViewerEvent { + static override readonly type = 'click'; + + /** @internal */ + constructor(public readonly data: ClickData) { + super(ClickEvent.type); + } +} + +/** + * @event Triggered when some options are changed + */ +export class ConfigChangedEvent extends ViewerEvent { + static override readonly type = 'config-changed'; + + /** @internal */ + constructor(public readonly options: Array) { + super(ConfigChangedEvent.type); + } + + /** + * Checks if at least one of the `options` has been modified + */ + containsOptions(...options: Array): boolean { + return options.some((option) => this.options.includes(option)); + } +} + +/** + * @event Triggered when the user double clicks on the viewer. The simple `click` event is always fired before `dblclick`. + */ +export class DoubleClickEvent extends ViewerEvent { + static override readonly type = 'dblclick'; + + /** @internal */ + constructor(public readonly data: ClickData) { + super(DoubleClickEvent.type); + } +} + +/** + * @event Triggered when the fullscreen is enabled/disabled + */ +export class FullscreenEvent extends ViewerEvent { + static override readonly type = 'fullscreen'; + + /** @internal */ + constructor(public readonly fullscreenEnabled: boolean) { + super(FullscreenEvent.type); + } +} + +/** + * @event Triggered when the notification is hidden + */ +export class HideNotificationEvent extends ViewerEvent { + static override readonly type = 'hide-notification'; + + /** @internal */ + constructor(public readonly notificationId?: string) { + super(HideNotificationEvent.type); + } +} + +/** + * @event Triggered when the overlay is hidden + */ +export class HideOverlayEvent extends ViewerEvent { + static override readonly type = 'hide-overlay'; + + /** @internal */ + constructor(public readonly overlayId?: string) { + super(HideOverlayEvent.type); + } +} + +/** + * @event Triggered when the panel is hidden + */ +export class HidePanelEvent extends ViewerEvent { + static override readonly type = 'hide-panel'; + + /** @internal */ + constructor(public readonly panelId?: string) { + super(HidePanelEvent.type); + } +} + +/** + * @event Triggered when a tooltip is hidden + */ +export class HideTooltipEvent extends ViewerEvent { + static override readonly type = 'hide-tooltip'; + + /** @internal */ + constructor( + /** Userdata associated to the tooltip */ + public readonly tooltipData: TooltipConfig['data'] + ) { + super(HideTooltipEvent.type); + } +} + +/** + * @event Triggered when a key is pressed, can be cancelled + */ +export class KeypressEvent extends ViewerEvent { + static override readonly type = 'key-press'; + + /** @internal */ + constructor(public readonly key: string) { + super(KeypressEvent.type, true); + } +} + +/** + * @event Triggered when the loader value changes + */ +export class LoadProgressEvent extends ViewerEvent { + static override readonly type = 'load-progress'; + + /** @internal */ + constructor(public readonly progress: number) { + super(LoadProgressEvent.type); + } +} + +/** + * @event Triggered when a panorama image has been loaded + */ +export class PanoramaLoadedEvent extends ViewerEvent { + static override readonly type = 'panorama-loaded'; + + /** @internal */ + constructor(public readonly data: TextureData) { + super(PanoramaLoadedEvent.type); + } +} + +/** + * @event Triggered when the view angles change + */ +export class PositionUpdatedEvent extends ViewerEvent { + static override readonly type = 'position-updated'; + + /** @internal */ + constructor(public readonly position: PositionCompat) { + super(PositionUpdatedEvent.type); + } +} + +/** + * @event Triggered when the panorama image has been loaded and the viewer is ready to perform the first render + */ +export class ReadyEvent extends ViewerEvent { + static override readonly type = 'ready'; + + /** @internal */ + constructor() { + super(ReadyEvent.type); + } +} + +/** + * @event Triggered on each viewer render + */ +export class RenderEvent extends ViewerEvent { + static override readonly type = 'render'; + + /** @internal */ + constructor() { + super(RenderEvent.type); + } +} + +/** + * @event Triggered when the notification is shown + */ +export class ShowNotificationEvent extends ViewerEvent { + static override readonly type = 'show-notification'; + + /** @internal */ + constructor(public readonly notificationId?: string) { + super(ShowNotificationEvent.type); + } +} + +/** + * @event Triggered when the overlay is shown + */ +export class ShowOverlayEvent extends ViewerEvent { + static override readonly type = 'show-overlay'; + + /** @internal */ + constructor(public readonly overlayId?: string) { + super(ShowOverlayEvent.type); + } +} + +/** + * @event Triggered when the panel is shown + */ +export class ShowPanelEvent extends ViewerEvent { + static override readonly type = 'show-panel'; + + /** @internal */ + constructor(public readonly panelId?: string) { + super(ShowPanelEvent.type); + } +} + +/** + * @event Triggered when a tooltip is shown + */ +export class ShowTooltipEvent extends ViewerEvent { + static override readonly type = 'show-tooltip'; + + /** @internal */ + constructor( + /** Instance of the tooltip */ + public readonly tooltip: Tooltip, + /** Userdata associated to the tooltip */ + public readonly tooltipData?: TooltipConfig['data'] + ) { + super(ShowTooltipEvent.type); + } +} + +/** + * @event Triggered when the viewer size changes + */ +export class SizeUpdatedEvent extends ViewerEvent { + static override readonly type = 'size-updated'; + + /** @internal */ + constructor(public readonly size: Size) { + super(SizeUpdatedEvent.type); + } +} + +/** + * @event Triggered when all current animations are stopped + */ +export class StopAllEvent extends ViewerEvent { + static override readonly type = 'stop-all'; + + /** @internal */ + constructor() { + super(StopAllEvent.type); + } +} + +/** + * @event Triggered when the viewer zoom changes + */ +export class ZoomUpdatedEvent extends ViewerEvent { + static override readonly type = 'zoom-updated'; + + /** @internal */ + constructor(public readonly zoomLevel: number) { + super(ZoomUpdatedEvent.type); + } +} + +/** + * Base class for events on three.js objects + * + * Note: {@link Viewer#observeObjects} must be called for these events to be dispatched + */ +export abstract class ObjectEvent extends ViewerEvent { + /** @internal */ + constructor( + type: string, + public readonly originalEvent: MouseEvent, + public readonly object: Mesh, + public readonly viewerPoint: Point, + public readonly userDataKey: string + ) { + super(type); + } +} + +/** + * @event Triggered when the cursor enters an object in the scene + * + * Note: {@link Viewer#observeObjects} must be called for this event to be dispatched + */ +export class ObjectEnterEvent extends ObjectEvent { + static override readonly type = 'enter-object'; + + /** @internal */ + constructor(originalEvent: MouseEvent, object: Mesh, viewerPoint: Point, userDataKey: string) { + super(ObjectEnterEvent.type, originalEvent, object, viewerPoint, userDataKey); + } +} + +/** + * @event Triggered when the cursor leaves an object in the scene + * + * Note: {@link Viewer#observeObjects} must be called for this event to be dispatched + */ +export class ObjectLeaveEvent extends ObjectEvent { + static override readonly type = 'leave-object'; + + /** @internal */ + constructor(originalEvent: MouseEvent, object: Mesh, viewerPoint: Point, userDataKey: string) { + super(ObjectLeaveEvent.type, originalEvent, object, viewerPoint, userDataKey); + } +} + +/** + * @event Triggered when the cursor moves over an object in the scene + * + * Note: {@link Viewer#observeObjects} must be called for this event to be dispatched + */ +export class ObjectHoverEvent extends ObjectEvent { + static override readonly type = 'hover-object'; + + /** @internal */ + constructor(originalEvent: MouseEvent, object: Mesh, viewerPoint: Point, userDataKey: string) { + super(ObjectHoverEvent.type, originalEvent, object, viewerPoint, userDataKey); + } +} + +export type ViewerEvents = + | BeforeAnimateEvent + | BeforeRenderEvent + | BeforeRotateEvent + | ClickEvent + | ConfigChangedEvent + | DoubleClickEvent + | FullscreenEvent + | HideNotificationEvent + | HideOverlayEvent + | HidePanelEvent + | HideTooltipEvent + | KeypressEvent + | LoadProgressEvent + | PanoramaLoadedEvent + | PositionUpdatedEvent + | ReadyEvent + | RenderEvent + | ShowNotificationEvent + | ShowOverlayEvent + | ShowPanelEvent + | SizeUpdatedEvent + | StopAllEvent + | ZoomUpdatedEvent + | ObjectEnterEvent + | ObjectLeaveEvent + | ObjectHoverEvent; diff --git a/src/icons/arrow.svg b/packages/core/src/icons/arrow.svg similarity index 100% rename from src/icons/arrow.svg rename to packages/core/src/icons/arrow.svg diff --git a/src/icons/download.svg b/packages/core/src/icons/download.svg similarity index 100% rename from src/icons/download.svg rename to packages/core/src/icons/download.svg diff --git a/src/icons/error.svg b/packages/core/src/icons/error.svg similarity index 100% rename from src/icons/error.svg rename to packages/core/src/icons/error.svg diff --git a/src/icons/fullscreen-in.svg b/packages/core/src/icons/fullscreen-in.svg similarity index 100% rename from src/icons/fullscreen-in.svg rename to packages/core/src/icons/fullscreen-in.svg diff --git a/src/icons/fullscreen-out.svg b/packages/core/src/icons/fullscreen-out.svg similarity index 100% rename from src/icons/fullscreen-out.svg rename to packages/core/src/icons/fullscreen-out.svg diff --git a/src/icons/gesture.svg b/packages/core/src/icons/gesture.svg similarity index 100% rename from src/icons/gesture.svg rename to packages/core/src/icons/gesture.svg diff --git a/src/icons/info.svg b/packages/core/src/icons/info.svg similarity index 100% rename from src/icons/info.svg rename to packages/core/src/icons/info.svg diff --git a/src/icons/menu.svg b/packages/core/src/icons/menu.svg similarity index 100% rename from src/icons/menu.svg rename to packages/core/src/icons/menu.svg diff --git a/src/icons/mousewheel.svg b/packages/core/src/icons/mousewheel.svg similarity index 100% rename from src/icons/mousewheel.svg rename to packages/core/src/icons/mousewheel.svg diff --git a/src/icons/zoom-in.svg b/packages/core/src/icons/zoom-in.svg similarity index 100% rename from src/icons/zoom-in.svg rename to packages/core/src/icons/zoom-in.svg diff --git a/src/icons/zoom-out.svg b/packages/core/src/icons/zoom-out.svg similarity index 100% rename from src/icons/zoom-out.svg rename to packages/core/src/icons/zoom-out.svg diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 000000000..48a673195 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,36 @@ +import * as CONSTANTS from './data/constants'; +import * as utils from './utils'; +import * as events from './events'; + +export type { AdapterConstructor } from './adapters/AbstractAdapter'; +export { AbstractAdapter } from './adapters/AbstractAdapter'; +export { EquirectangularAdapter } from './adapters/EquirectangularAdapter'; +export type { EquirectangularAdapterConfig } from './adapters/EquirectangularAdapter'; +export type { ButtonConfig } from './buttons/AbstractButton'; +export type { ButtonConstructor } from './buttons/AbstractButton'; +export { AbstractButton } from './buttons/AbstractButton'; +export { AbstractComponent } from './components/AbstractComponent'; +export type { Tooltip, TooltipConfig, TooltipPosition } from './components/Tooltip'; +export type { Loader } from './components/Loader'; +export type { Navbar } from './components/Navbar'; +export { registerButton } from './components/Navbar'; +export type { Notification, NotificationConfig } from './components/Notification'; +export type { Overlay, OverlayConfig } from './components/Overlay'; +export type { Panel, PanelConfig } from './components/Panel'; +export { DEFAULTS } from './data/config'; +export { SYSTEM } from './data/system'; +export type { TypedEventTarget } from './lib/TypedEventTarget'; +export { TypedEvent } from './lib/TypedEventTarget'; +export type { PluginConstructor } from './plugins/AbstractPlugin'; +export { AbstractPlugin } from './plugins/AbstractPlugin'; +export type { DataHelper } from './services/DataHelper'; +export type { Renderer } from './services/Renderer'; +export type { TextureLoader } from './services/TextureLoader'; +export type { ViewerState } from './services/ViewerState'; +export { PSVError } from './PSVError'; +export { Viewer } from './Viewer'; +export * from './model'; +export { CONSTANTS, events, utils }; + +/** @internal */ +import './styles/index.scss'; diff --git a/packages/core/src/lib/TypedEventTarget.ts b/packages/core/src/lib/TypedEventTarget.ts new file mode 100644 index 000000000..46dc520ad --- /dev/null +++ b/packages/core/src/lib/TypedEventTarget.ts @@ -0,0 +1,50 @@ +/** + * Base class for events dispatched by {@link TypedEventTarget} + * @template TTarget type of the event target + */ +export abstract class TypedEvent> extends Event { + static readonly type: string; + + override get target(): TTarget { + return super.target as any; + } + + constructor(type: string, cancelable = false) { + super(type, { cancelable }); + } +} + +/** + * Decorator for EventTarget allowing to strongly type events and listeners + * @link https://rjzaworski.com/2021/06/event-target-with-typescript + * @template TEvents union of dispatched events + */ +export class TypedEventTarget> extends EventTarget { + override dispatchEvent(e: TEvents): boolean { + return super.dispatchEvent(e); + } + + /** + * @template T the name of event + * @template E the class of the event + */ + override addEventListener( + type: T, + callback: ((e: E) => void) | EventListenerObject, + options?: AddEventListenerOptions | boolean + ) { + super.addEventListener(type, callback as any, options); + } + + /** + * @template T the name of event + * @template E the class of the event + */ + override removeEventListener( + type: TEvents['type'], + callback: ((e: E) => void) | EventListenerObject, + options?: EventListenerOptions | boolean + ) { + super.removeEventListener(type, callback as any, options); + } +} diff --git a/packages/core/src/model.ts b/packages/core/src/model.ts new file mode 100644 index 000000000..099bf60b3 --- /dev/null +++ b/packages/core/src/model.ts @@ -0,0 +1,429 @@ +import { Object3D, Texture } from 'three'; +import { AdapterConstructor } from './adapters/AbstractAdapter'; +import { ACTIONS } from './data/constants'; +import { PluginConstructor } from './plugins/AbstractPlugin'; +import { Viewer } from './Viewer'; + +/** + * A wrapper around a Promise with an initial value before resolution + */ +export type InitialPromise = { initial: T; promise: Promise }; + +/** + * Object defining a point + */ +export type Point = { + x: number; + y: number; +}; + +/** + * Object defining a size + */ +export type Size = { + width: number; + height: number; +}; + +/** + * Object defining a size in CSS + */ +export type CssSize = { + width: string; + height: string; +}; + +/** + * Object defining angular corrections to a sphere + */ +export type SphereCorrection = { + pan?: number; + tilt?: number; + roll?: number; +}; + +/** + * Object defining a spherical position (radians) + */ +export type Position = { + yaw: number; + pitch: number; +}; + +/** + * @deprecated + */ +export type PositionCompat = Position & { + /** + * @deprecated use `yaw` + */ + longitude?: number; + /** + * @deprecated use `pitch` + */ + latitude?: number; +}; + +/** + * Object defining a spherical position (radians or degrees) + */ +export type SphericalPosition = { + yaw: number | string; + pitch: number | string; +}; + +/** + * Object defining a position on the panorama image (pixels) + */ +export type PanoramaPosition = { + textureX: number; + textureY: number; +}; + +/** + * Object defining a spherical or panorama position + */ +export type ExtendedPosition = SphericalPosition | PanoramaPosition; + +/** + * Object defining options for {@link Viewer.animate} + */ +export type AnimateOptions = Partial & { + /** + * Animation speed or duration in milliseconds + */ + speed: string | number; + /** + * New zoom level between 0 and 100 + */ + zoom?: number; +}; + +/** + * Crop information of an equirectangular panorama + */ +export type PanoData = { + fullWidth: number; + fullHeight: number; + croppedWidth: number; + croppedHeight: number; + croppedX: number; + croppedY: number; + poseHeading?: number; + posePitch?: number; + poseRoll?: number; +}; + +/** + * Function to compute panorama data once the image is loaded + */ +export type PanoDataProvider = (image: HTMLImageElement) => PanoData; + +/** + * Object defining options for {@link Viewer.setPanorama} + */ +export type PanoramaOptions = Partial & { + /** + * new navbar caption + */ + caption?: string; + /** + * new ppanorama description + */ + description?: string; + /** + * new zoom level between 0 and 100 + */ + zoom?: number; + /** + * duration of the transition between all and new panorama + * @default 1500 + */ + transition?: boolean | number; + /** + * show the loader while loading the new panorama + * @default true + */ + showLoader?: boolean; + /** + * new sphere correction to apply to the panorama + */ + sphereCorrection?: SphereCorrection; + /** + * new data used for this panorama + */ + panoData?: PanoData | PanoDataProvider; + /** + * new overlay to apply to the panorama + */ + overlay?: any; + /** + * new overlay opacity + */ + overlayOpacity?: number; +}; + +/** + * Result of {@link AbstractAdapter.loadTexture} + */ +export type TextureData> = { + texture: T; + panoData?: PanoData; + panorama: any; +}; + +/** + * Data of {@link events.ClickEvent} + */ +export type ClickData = { + /** + * if it's a right click + */ + rightclick: boolean; + /** + * position in the browser window + */ + clientX: number; + /** + * position in the browser window + */ + clientY: number; + /** + * position in the viewer + */ + viewerX: number; + /** + * position in the viewer + */ + viewerY: number; + /** + * position in spherical coordinates + */ + yaw: number; + /** + * position in spherical coordinates + */ + pitch: number; + /** + * position on the texture, if applicable + */ + textureX?: number; + /** + * position on the texture, if applicable + */ + textureY?: number; + /** + * Original element which received the click + */ + target: HTMLElement; + /** + * List of THREE scenes objects under the mouse + */ + objects: Object3D[]; + /** + * clicked Marker + */ + marker?: any; + + /** + * @deprecated use `yaw` + */ + longitude?: number; + /** + * @deprecated use `pitch` + */ + latitude?: number; +}; + +/** + * Definition of a custom navbar button + */ +export type NavbarCustomButton = { + /** + * Unique identifier of the button, usefull when using the {@link Navbar.getButton} method + */ + id?: string; + /** + * Tooltip displayed when the mouse is over the button + */ + title?: string; + /** + * Content of the button. Preferably a square image or SVG icon + */ + content: string; + /** + * CSS class added to the button + */ + className?: string; + /** + * Function called when the button is clicked + */ + onClick: (viewer: Viewer) => void; + /** + * initial state of the button + * @default false + */ + disabled?: boolean; + /** + * initial visibility of the button + * @default true + */ + visible?: boolean; + /** + * if the button can be moved to menu when the navbar is too small + * @default true + */ + collapsable?: boolean; + /** + * if the button is accessible with the keyboard + * @default true + */ + tabbable?: boolean; +}; + +/** + * Viewer configuration + * @link https://photo-sphere-viewer.js.org/guide/config.html + */ +export type ViewerConfig = { + container: HTMLElement | string; + panorama?: any; + overlay?: any; + /** @default 1 */ + overlayOpacity?: number; + /** @default equirectangular */ + adapter?: AdapterConstructor | [AdapterConstructor, any]; + plugins?: Array; + /** @default null */ + caption?: string; + /** @default null */ + description?: string; + /** @default null */ + downloadUrl?: string; + /** @default null */ + loadingImg?: string; + /** @default 'Loading...' */ + loadingTxt?: string; + /** @default `container` size */ + size?: CssSize; + /** @default false */ + fisheye?: boolean | number; + /** @default 30 */ + minFov?: number; + /** @default 90 */ + maxFov?: number; + /** @default 50 */ + defaultZoomLvl?: number; + /** @deprecated use `defaultYaw` */ + defaultLong?: number; + /** @deprecated use `defaultPitch` */ + defaultLat?: number; + /** @default 0 */ + defaultYaw?: number | string; + /** @default 0 */ + defaultPitch?: number | string; + /** @default `0,0,0` */ + sphereCorrection?: SphereCorrection; + /** @default 1 */ + moveSpeed?: number; + /** @default 1 */ + zoomSpeed?: number; + /** @deprecated use the 'autorotate' plugin */ + autorotateDelay?: number | null; + /** @deprecated use the 'autorotate' plugin */ + autorotateIdle?: boolean; + /** @deprecated use the 'autorotate' plugin */ + autorotateSpeed?: string | number; + /** @deprecated use the 'autorotate' plugin */ + autorotateLat?: number; + /** @deprecated use the 'autorotate' plugin */ + autorotateZoomLvl?: number; + /** @default true */ + moveInertia?: boolean; + /** @default true */ + mousewheel?: boolean; + /** @default true */ + mousemove?: boolean; + /** @default false */ + mousewheelCtrlKey?: boolean; + /** @default false */ + touchmoveTwoFingers?: boolean; + /** @default true */ + useXmpData?: boolean; + panoData?: PanoData | PanoDataProvider; + requestHeaders?: Record | ((url: string) => Record); + /** @default '#000' */ + canvasBackground?: string; + /** @default false */ + withCredentials?: boolean; + /** @default 'autorotate zoom move download description caption fullscreen' */ + navbar?: boolean | string | Array; + lang?: { + zoom: string; + zoomOut: string; + zoomIn: string; + moveUp: string; + moveDown: string; + moveLeft: string; + moveRight: string; + download: string; + fullscreen: string; + menu: string; + twoFingers: string; + ctrlZoom: string; + loadError: string; + [K: string]: string; + }; + keyboard?: boolean | Record; +}; + +export type DeprecatedViewerConfig = + | 'defaultLong' + | 'defaultLat' + | 'autorotateDelay' + | 'autorotateIdle' + | 'autorotateSpeed' + | 'autorotateLat' + | 'autorotateZoomLvl'; + +/** + * Viewer configuration after applying parsers + */ +export type ParsedViewerConfig = Omit< + ViewerConfig, + | 'adapter' + | 'plugins' + | 'defaultYaw' + | 'defaultPitch' + | 'fisheye' + | 'requestHeaders' + | 'navbar' + | 'keyboard' + | DeprecatedViewerConfig +> & { + adapter?: [AdapterConstructor, any]; + plugins?: Array<[PluginConstructor, any]>; + defaultYaw?: number; + defaultPitch?: number; + fisheye?: number; + requestHeaders?: (url: string) => Record; + navbar?: Array; + keyboard?: Record; +}; + +/** + * Readonly viewer configuration + */ +export type ReadonlyViewerConfig = + | 'panorama' + | 'panoData' + | 'overlay' + | 'overlayOpacity' + | 'container' + | 'adapter' + | 'plugins'; + +/** + * Updatable viewer configuration + */ +export type UpdatableViewerConfig = Omit; diff --git a/packages/core/src/plugins/AbstractPlugin.ts b/packages/core/src/plugins/AbstractPlugin.ts new file mode 100644 index 000000000..514f7af55 --- /dev/null +++ b/packages/core/src/plugins/AbstractPlugin.ts @@ -0,0 +1,48 @@ +import { TypedEvent, TypedEventTarget } from '../lib/TypedEventTarget'; +import type { Viewer } from '../Viewer'; + +/** + * Base class for plugins + * @template TEvents union of dispatched events + */ +export abstract class AbstractPlugin< + TEvents extends TypedEvent = never +> extends TypedEventTarget { + /** + * Unique identifier of the plugin + */ + static readonly id: string; + + constructor(protected viewer: Viewer) { + super(); + } + + /** + * Initializes the plugin + */ + // eslint-disable-next-line @typescript-eslint/no-empty-function + init() {} + + /** + * Destroys the plugin + */ + // eslint-disable-next-line @typescript-eslint/no-empty-function + destroy() {} +} + +export type PluginConstructor = (new (viewer: Viewer, config?: any) => AbstractPlugin) & typeof AbstractPlugin; + +/** + * Returns the plugin constructor from the imported object + * @internal + */ +export function pluginInterop(plugin: any): PluginConstructor { + if (plugin) { + for (const [, p] of [['_', plugin], ...Object.entries(plugin)]) { + if (p.prototype instanceof AbstractPlugin) { + return p; + } + } + } + return null; +} diff --git a/packages/core/src/services/AbstractService.ts b/packages/core/src/services/AbstractService.ts new file mode 100644 index 000000000..da08d8a5d --- /dev/null +++ b/packages/core/src/services/AbstractService.ts @@ -0,0 +1,26 @@ +import { ParsedViewerConfig } from '../model'; +import type { Viewer } from '../Viewer'; +import { ViewerState } from './ViewerState'; + +/** + * Base class for services + */ +export abstract class AbstractService { + protected readonly config: ParsedViewerConfig; + protected readonly state: ViewerState; + + /** + * @internal + */ + constructor(protected readonly viewer: Viewer) { + this.config = viewer.config; + this.state = viewer.state; + } + + /** + * Destroys the service + * @internal + */ + // eslint-disable-next-line @typescript-eslint/no-empty-function + destroy() {} +} diff --git a/packages/core/src/services/DataHelper.ts b/packages/core/src/services/DataHelper.ts new file mode 100644 index 000000000..adfe0883a --- /dev/null +++ b/packages/core/src/services/DataHelper.ts @@ -0,0 +1,244 @@ +import { Euler, MathUtils, Vector3 } from 'three'; +import { SPHERE_RADIUS, VIEWER_DATA } from '../data/constants'; +import { + ExtendedPosition, + PanoData, + PanoramaPosition, + Point, + Position, + SphereCorrection, + SphericalPosition, +} from '../model'; +import { PSVError } from '../PSVError'; +import { applyEulerInverse, logWarn, parseAngle, parseSpeed } from '../utils'; +import type { Viewer } from '../Viewer'; +import { AbstractService } from './AbstractService'; + +const vector3 = new Vector3(); +const EULER_ZERO = new Euler(0, 0, 0, 'ZXY'); + +/** + * Collections of data converters for the viewer + */ +export class DataHelper extends AbstractService { + /** + * @internal + */ + constructor(viewer: Viewer) { + super(viewer); + } + + /** + * Converts vertical FOV to zoom level + */ + fovToZoomLevel(fov: number): number { + const temp = Math.round(((fov - this.config.minFov) / (this.config.maxFov - this.config.minFov)) * 100); + return temp - 2 * (temp - 50); + } + + /** + * Converts zoom level to vertical FOV + */ + zoomLevelToFov(level: number): number { + return this.config.maxFov + (level / 100) * (this.config.minFov - this.config.maxFov); + } + + /** + * Converts vertical FOV to horizontal FOV + */ + vFovToHFov(vFov: number): number { + return MathUtils.radToDeg(2 * Math.atan(Math.tan(MathUtils.degToRad(vFov) / 2) * this.state.aspect)); + } + + /** + * Converts a speed into a duration from current position to a new position + */ + speedToDuration(value: string | number, angle: number): number { + if (typeof value !== 'number') { + // desired radial speed + const speed = parseSpeed(value); + // compute duration + return (angle / Math.abs(speed)) * 1000; + } else { + return Math.abs(value); + } + } + + /** + * Converts pixel texture coordinates to spherical radians coordinates + * @throws {@link PSVError} when the current adapter does not support texture coordinates + */ + textureCoordsToSphericalCoords(point: PanoramaPosition): Position { + const panoData = this.state.panoData; + if (!panoData) { + throw new PSVError('Current adapter does not support texture coordinates.'); + } + + const relativeX = ((point.textureX + panoData.croppedX) / panoData.fullWidth) * Math.PI * 2; + const relativeY = ((point.textureY + panoData.croppedY) / panoData.fullHeight) * Math.PI; + + const result: Position = { + yaw: relativeX >= Math.PI ? relativeX - Math.PI : relativeX + Math.PI, + pitch: Math.PI / 2 - relativeY, + }; + + // Apply panoData pose and sphereCorrection + if ( + !EULER_ZERO.equals(this.viewer.renderer.panoramaPose) + || !EULER_ZERO.equals(this.viewer.renderer.sphereCorrection) + ) { + this.sphericalCoordsToVector3(result, vector3); + vector3.applyEuler(this.viewer.renderer.panoramaPose); + vector3.applyEuler(this.viewer.renderer.sphereCorrection); + return this.vector3ToSphericalCoords(vector3); + } else { + return result; + } + } + + /** + * Converts spherical radians coordinates to pixel texture coordinates + * @throws {@link PSVError} when the current adapter does not support texture coordinates + */ + sphericalCoordsToTextureCoords(position: Position): PanoramaPosition { + const panoData = this.state.panoData; + if (!panoData) { + throw new PSVError('Current adapter does not support texture coordinates.'); + } + + // Apply panoData pose and sphereCorrection + if ( + !EULER_ZERO.equals(this.viewer.renderer.panoramaPose) + || !EULER_ZERO.equals(this.viewer.renderer.sphereCorrection) + ) { + this.sphericalCoordsToVector3(position, vector3); + applyEulerInverse(vector3, this.viewer.renderer.sphereCorrection); + applyEulerInverse(vector3, this.viewer.renderer.panoramaPose); + position = this.vector3ToSphericalCoords(vector3); + } + + const relativeLong = (position.yaw / Math.PI / 2) * panoData.fullWidth; + const relativeLat = (position.pitch / Math.PI) * panoData.fullHeight; + + return { + textureX: + Math.round( + position.yaw < Math.PI + ? relativeLong + panoData.fullWidth / 2 + : relativeLong - panoData.fullWidth / 2 + ) - panoData.croppedX, + textureY: Math.round(panoData.fullHeight / 2 - relativeLat) - panoData.croppedY, + }; + } + + /** + * Converts spherical radians coordinates to a Vector3 + */ + sphericalCoordsToVector3(position: Position, vector?: Vector3): Vector3 { + if (!vector) { + vector = new Vector3(); + } + vector.x = SPHERE_RADIUS * -Math.cos(position.pitch) * Math.sin(position.yaw); + vector.y = SPHERE_RADIUS * Math.sin(position.pitch); + vector.z = SPHERE_RADIUS * Math.cos(position.pitch) * Math.cos(position.yaw); + return vector; + } + + /** + * Converts a Vector3 to spherical radians coordinates + */ + vector3ToSphericalCoords(vector: Vector3): Position { + const phi = Math.acos(vector.y / Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z)); + const theta = Math.atan2(vector.x, vector.z); + + return { + yaw: theta < 0 ? -theta : Math.PI * 2 - theta, + pitch: Math.PI / 2 - phi, + }; + } + + /** + * Converts position on the viewer to a THREE.Vector3 + */ + viewerCoordsToVector3(viewerPoint: Point): Vector3 { + const sphereIntersect = this.viewer.renderer + .getIntersections(viewerPoint) + .filter((i) => i.object.userData[VIEWER_DATA]); + + if (sphereIntersect.length) { + return sphereIntersect[0].point; + } else { + return null; + } + } + + /** + * Converts a Vector3 to position on the viewer + */ + vector3ToViewerCoords(vector: Vector3): Point { + const vectorClone = vector.clone(); + vectorClone.project(this.viewer.renderer.camera); + + return { + x: Math.round(((vectorClone.x + 1) / 2) * this.state.size.width), + y: Math.round(((1 - vectorClone.y) / 2) * this.state.size.height), + }; + } + + /** + * Converts spherical radians coordinates to position on the viewer + */ + sphericalCoordsToViewerCoords(position: Position): Point { + this.sphericalCoordsToVector3(position, vector3); + return this.vector3ToViewerCoords(vector3); + } + + /** + * Converts pixel position to angles if present and ensure boundaries + */ + cleanPosition(position: ExtendedPosition): Position { + if ('x' in position && 'y' in position) { + logWarn('x/y position is deprecated, use textureX/textureY instead'); + return this.textureCoordsToSphericalCoords({ + textureX: position['x'] as any, + textureY: position['y'] as any, + }); + } + if ('longitude' in position && 'latitude' in position) { + logWarn('longitude/latitude position is deprecated, use yaw/pitch instead'); + return this.cleanPosition({ yaw: position['longitude'] as any, pitch: position['latitude'] as any }); + } + if ( + (position as PanoramaPosition).textureX !== undefined + && (position as PanoramaPosition).textureY !== undefined + ) { + return this.textureCoordsToSphericalCoords(position as PanoramaPosition); + } + return { + yaw: parseAngle((position as SphericalPosition).yaw), + pitch: parseAngle((position as SphericalPosition).pitch, !this.state.littlePlanet), + }; + } + + /** + * Ensure a SphereCorrection object is valid + */ + cleanSphereCorrection(sphereCorrection: SphereCorrection): SphereCorrection { + return { + pan: parseAngle(sphereCorrection?.pan || 0), + tilt: parseAngle(sphereCorrection?.tilt || 0, true), + roll: parseAngle(sphereCorrection?.roll || 0, true, false), + }; + } + + /** + * Parse the pose angles of the pano data + */ + cleanPanoramaPose(panoData: PanoData): SphereCorrection { + return { + pan: MathUtils.degToRad(panoData?.poseHeading || 0), + tilt: MathUtils.degToRad(panoData?.posePitch || 0), + roll: MathUtils.degToRad(panoData?.poseRoll || 0), + }; + } +} diff --git a/packages/core/src/services/EventsHandler.ts b/packages/core/src/services/EventsHandler.ts new file mode 100644 index 000000000..9c3734a89 --- /dev/null +++ b/packages/core/src/services/EventsHandler.ts @@ -0,0 +1,698 @@ +import { MathUtils, Mesh, SplineCurve, Vector2 } from 'three'; +import { + ACTIONS, + CTRLZOOM_TIMEOUT, + DBLCLICK_DELAY, + IDS, + INERTIA_WINDOW, + KEY_CODES, + LONGTOUCH_DELAY, + MOVE_THRESHOLD, + TWOFINGERSOVERLAY_DELAY, + VIEWER_DATA, +} from '../data/constants'; +import { SYSTEM } from '../data/system'; +import { + ClickEvent, + DoubleClickEvent, + FullscreenEvent, + KeypressEvent, + ObjectEnterEvent, + ObjectEvent, + ObjectHoverEvent, + ObjectLeaveEvent, +} from '../events'; +import gestureIcon from '../icons/gesture.svg'; +import mousewheelIcon from '../icons/mousewheel.svg'; +import { ClickData, Point, Position } from '../model'; +import { + Animation, + clone, + distance, + getClosest, + getPosition, + hasParent, + isEmpty, + positionCompat, + throttle, +} from '../utils'; +import { PressHandler } from '../utils/PressHandler'; +import type { Viewer } from '../Viewer'; +import { AbstractService } from './AbstractService'; + +const IDLE = 0; +const MOVING = 1; +const INERTIA = 2; + +/** + * Events handler + * @internal + */ +export class EventsHandler extends AbstractService { + private readonly data = { + step: IDLE, + /** before moving past the threshold */ + mousedown: false, + /** start x position of the click/touch */ + startMouseX: 0, + /** start y position of the click/touch */ + startMouseY: 0, + /** current x position of the cursor */ + mouseX: 0, + /** current y position of the cursor */ + mouseY: 0, + /** list of latest positions of the cursor, [time, x, y] */ + mouseHistory: [] as [number, number, number][], + /** distance between fingers when zooming */ + pinchDist: 0, + /** when the Ctrl key is pressed */ + ctrlKeyDown: false, + /** temporary storage of click data between two clicks */ + dblclickData: null as ClickData, + dblclickTimeout: null as ReturnType, + longtouchTimeout: null as ReturnType, + twofingersTimeout: null as ReturnType, + ctrlZoomTimeout: null as ReturnType, + }; + + private readonly keyHandler = new PressHandler(); + private readonly resizeObserver = new ResizeObserver(throttle(() => this.viewer.autoSize(), 50)); + private readonly moveThreshold = MOVE_THRESHOLD * SYSTEM.pixelRatio; + + constructor(viewer: Viewer) { + super(viewer); + } + + /** + * @internal + */ + init() { + window.addEventListener('keydown', this, { passive: false }); + window.addEventListener('keyup', this); + this.viewer.container.addEventListener('mousedown', this); + window.addEventListener('mousemove', this, { passive: false }); + window.addEventListener('mouseup', this); + this.viewer.container.addEventListener('touchstart', this, { passive: false }); + window.addEventListener('touchmove', this, { passive: false }); + window.addEventListener('touchend', this, { passive: false }); + this.viewer.container.addEventListener('wheel', this, { passive: false }); + document.addEventListener('fullscreenchange', this); + this.resizeObserver.observe(this.viewer.container); + } + + override destroy() { + window.removeEventListener('keydown', this); + window.removeEventListener('keyup', this); + this.viewer.container.removeEventListener('mousedown', this); + window.removeEventListener('mousemove', this); + window.removeEventListener('mouseup', this); + this.viewer.container.removeEventListener('touchstart', this); + window.removeEventListener('touchmove', this); + window.removeEventListener('touchend', this); + this.viewer.container.removeEventListener('wheel', this); + document.removeEventListener('fullscreenchange', this); + this.resizeObserver.disconnect(); + + clearTimeout(this.data.dblclickTimeout); + clearTimeout(this.data.longtouchTimeout); + clearTimeout(this.data.twofingersTimeout); + clearTimeout(this.data.ctrlZoomTimeout); + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(evt: Event) { + // prettier-ignore + switch (evt.type) { + case 'keydown': this.__onKeyDown(evt as KeyboardEvent); break; + case 'keyup': this.__onKeyUp(); break; + case 'mousemove': this.__onMouseMove(evt as MouseEvent); break; + case 'mouseup': this.__onMouseUp(evt as MouseEvent); break; + case 'touchmove': this.__onTouchMove(evt as TouchEvent); break; + case 'touchend': this.__onTouchEnd(evt as TouchEvent); break; + case 'fullscreenchange': this.__onFullscreenChange(); break; + } + + if (!getClosest(evt.target as HTMLElement, '.psv--capture-event')) { + // prettier-ignore + switch (evt.type) { + case 'mousedown': this.__onMouseDown(evt as MouseEvent); break; + case 'touchstart': this.__onTouchStart(evt as TouchEvent); break; + case 'wheel': this.__onMouseWheel(evt as WheelEvent); break; + } + } + } + + /** + * Handles keyboard events + */ + private __onKeyDown(e: KeyboardEvent) { + if (this.config.mousewheelCtrlKey) { + this.data.ctrlKeyDown = e.key === KEY_CODES.Control; + + if (this.data.ctrlKeyDown) { + clearTimeout(this.data.ctrlZoomTimeout); + this.viewer.overlay.hide(IDS.CTRL_ZOOM); + } + } + + if (!this.viewer.dispatchEvent(new KeypressEvent(e.key))) { + return; + } + + if (!this.state.keyboardEnabled) { + return; + } + + const action = this.config.keyboard[e.key]; + if (action && !this.keyHandler.pending) { + if (action !== ACTIONS.ZOOM_IN && action !== ACTIONS.ZOOM_OUT) { + this.viewer.stopAll(); + } + + // prettier-ignore + switch (action) { + case ACTIONS.ROTATE_UP: this.viewer.dynamics.position.roll({ pitch: false }); break; + case ACTIONS.ROTATE_DOWN: this.viewer.dynamics.position.roll({ pitch: true }); break; + case ACTIONS.ROTATE_RIGHT: this.viewer.dynamics.position.roll({ yaw: false }); break; + case ACTIONS.ROTATE_LEFT: this.viewer.dynamics.position.roll({ yaw: true }); break; + case ACTIONS.ZOOM_IN: this.viewer.dynamics.zoom.roll(false); break; + case ACTIONS.ZOOM_OUT: this.viewer.dynamics.zoom.roll(true); break; + } + + this.keyHandler.down(); + e.preventDefault(); + } + } + + /** + * Handles keyboard events + */ + private __onKeyUp() { + this.data.ctrlKeyDown = false; + + if (!this.state.keyboardEnabled) { + return; + } + + this.keyHandler.up(() => { + this.viewer.dynamics.position.stop(); + this.viewer.dynamics.zoom.stop(); + this.viewer.resetIdleTimer(); + }); + } + + /** + * Handles mouse down events + */ + private __onMouseDown(evt: MouseEvent) { + this.data.mousedown = true; + this.data.startMouseX = evt.clientX; + this.data.startMouseY = evt.clientY; + } + + /** + *Handles mouse up events + */ + private __onMouseUp(evt: MouseEvent) { + if (this.data.mousedown || this.data.step === MOVING) { + this.__stopMove(evt.clientX, evt.clientY, evt.target, evt.button === 2); + } + } + + /** + * Handles mouse move events + */ + private __onMouseMove(evt: MouseEvent) { + if (this.config.mousemove && (this.data.mousedown || this.data.step === MOVING)) { + evt.preventDefault(); + this.__doMove(evt.clientX, evt.clientY); + } + + this.__handleObjectsEvents(evt); + } + + /** + * Handles touch events + */ + private __onTouchStart(evt: TouchEvent) { + if (evt.touches.length === 1) { + this.data.mousedown = true; + this.data.startMouseX = evt.touches[0].clientX; + this.data.startMouseY = evt.touches[0].clientY; + + if (!this.data.longtouchTimeout) { + this.data.longtouchTimeout = setTimeout(() => { + const touch = evt.touches[0]; + this.__stopMove(touch.clientX, touch.clientY, touch.target, true); + this.data.longtouchTimeout = null; + }, LONGTOUCH_DELAY); + } + } else if (evt.touches.length === 2) { + this.data.mousedown = false; + this.__cancelLongTouch(); + + if (this.config.mousemove) { + this.__cancelTwoFingersOverlay(); + this.__startMoveZoom(evt); + evt.preventDefault(); + } + } + } + + /** + * Handles touch events + */ + private __onTouchEnd(evt: TouchEvent) { + this.__cancelLongTouch(); + + if (this.data.mousedown || this.data.step === MOVING) { + evt.preventDefault(); + this.__cancelTwoFingersOverlay(); + + if (evt.touches.length === 1) { + this.__stopMove(this.data.mouseX, this.data.mouseY); + } else if (evt.touches.length === 0) { + const touch = evt.changedTouches[0]; + this.__stopMove(touch.clientX, touch.clientY, touch.target); + } + } + } + + /** + * Handles touch move events + */ + private __onTouchMove(evt: TouchEvent) { + this.__cancelLongTouch(); + + if (!this.config.mousemove) { + return; + } + + if (evt.touches.length === 1) { + if (this.config.touchmoveTwoFingers) { + if (this.data.mousedown && !this.data.twofingersTimeout) { + this.data.twofingersTimeout = setTimeout(() => { + this.viewer.overlay.show({ + id: IDS.TWO_FINGERS, + image: gestureIcon, + title: this.config.lang.twoFingers, + }); + }, TWOFINGERSOVERLAY_DELAY); + } + } else if (this.data.mousedown || this.data.step === MOVING) { + evt.preventDefault(); + const touch = evt.touches[0]; + this.__doMove(touch.clientX, touch.clientY); + } + } else { + this.__doMoveZoom(evt); + this.__cancelTwoFingersOverlay(); + } + } + + /** + * Cancel the long touch timer if any + */ + private __cancelLongTouch() { + if (this.data.longtouchTimeout) { + clearTimeout(this.data.longtouchTimeout); + this.data.longtouchTimeout = null; + } + } + + /** + * Cancel the two fingers overlay timer if any + */ + private __cancelTwoFingersOverlay() { + if (this.config.touchmoveTwoFingers) { + if (this.data.twofingersTimeout) { + clearTimeout(this.data.twofingersTimeout); + this.data.twofingersTimeout = null; + } + this.viewer.overlay.hide(IDS.TWO_FINGERS); + } + } + + /** + * Handles mouse wheel events + */ + private __onMouseWheel(evt: WheelEvent) { + if (!this.config.mousewheel) { + return; + } + + if (this.config.mousewheelCtrlKey && !this.data.ctrlKeyDown) { + this.viewer.overlay.show({ + id: IDS.CTRL_ZOOM, + image: mousewheelIcon, + title: this.config.lang.ctrlZoom, + }); + + clearTimeout(this.data.ctrlZoomTimeout); + this.data.ctrlZoomTimeout = setTimeout(() => this.viewer.overlay.hide(IDS.CTRL_ZOOM), CTRLZOOM_TIMEOUT); + + return; + } + + evt.preventDefault(); + evt.stopPropagation(); + + const delta = (evt.deltaY / Math.abs(evt.deltaY)) * 5 * this.config.zoomSpeed; + if (delta !== 0) { + this.viewer.dynamics.zoom.step(-delta, 5); + } + } + + /** + * Handles fullscreen events + */ + private __onFullscreenChange() { + const fullscreen = this.viewer.isFullscreenEnabled(); + + if (this.config.keyboard) { + if (fullscreen) { + this.viewer.startKeyboardControl(); + } else { + this.viewer.stopKeyboardControl(); + } + } + + this.viewer.dispatchEvent(new FullscreenEvent(fullscreen)); + } + + /** + * Resets all state variables + */ + private __resetMove() { + this.data.step = IDLE; + this.data.mousedown = false; + this.data.mouseX = 0; + this.data.mouseY = 0; + this.data.startMouseX = 0; + this.data.startMouseY = 0; + this.data.mouseHistory.length = 0; + } + + /** + * Initializes the combines move and zoom + */ + private __startMoveZoom(evt: TouchEvent) { + this.viewer.stopAll(); // TODO nom ? + this.__resetMove(); + + const p1 = { x: evt.touches[0].clientX, y: evt.touches[0].clientY }; + const p2 = { x: evt.touches[1].clientX, y: evt.touches[1].clientY }; + + this.data.step = MOVING; + this.data.pinchDist = distance(p1, p2); + this.data.mouseX = (p1.x + p2.x) / 2; + this.data.mouseY = (p1.y + p2.y) / 2; + this.__logMouseMove(this.data.mouseX, this.data.mouseY); + } + + /** + * Stops the movement + * @description If the move threshold was not reached a click event is triggered, otherwise an animation is launched to simulate inertia + */ + private __stopMove(clientX: number, clientY: number, target?: EventTarget, rightclick = false) { + if (this.data.step === MOVING) { + if (this.config.moveInertia) { + this.__logMouseMove(clientX, clientY); + this.__stopMoveInertia(clientX, clientY); + } else { + this.__resetMove(); + this.viewer.resetIdleTimer(); + } + } else if (this.data.mousedown) { + this.viewer.stopAnimation(); + this.__doClick(clientX, clientY, target, rightclick); + this.__resetMove(); + this.viewer.resetIdleTimer(); + } + } + + /** + * Performs an animation to simulate inertia when the movement stops + */ + private __stopMoveInertia(clientX: number, clientY: number) { + // get direction at end of movement + const curve = new SplineCurve(this.data.mouseHistory.map(([, x, y]) => new Vector2(x, y))); + const direction = curve.getTangent(1); + + // average speed + // prettier-ignore + const speed = this.data.mouseHistory.reduce(({ total, prev }, curr) => ({ + total: !prev ? 0 : total + distance({ x: prev[1], y: prev[2] }, { x: curr[1], y: curr[2] }) / (curr[0] - prev[0]), + prev: curr, + }), { + total: 0, + prev: null, + }).total / this.data.mouseHistory.length; + + if (!speed) { + this.__resetMove(); + this.viewer.resetIdleTimer(); + return; + } + + this.data.step = INERTIA; + + let currentClientX = clientX; + let currentClientY = clientY; + + this.state.animation = new Animation({ + properties: { + speed: { start: speed, end: 0 }, + }, + duration: 1000, + easing: 'outQuad', + onTick: (properties) => { + // 3 is a magic number + currentClientX += properties.speed * direction.x * 3 * SYSTEM.pixelRatio; + currentClientY += properties.speed * direction.y * 3 * SYSTEM.pixelRatio; + this.__applyMove(currentClientX, currentClientY); + }, + }); + + this.state.animation.then((done) => { + this.state.animation = null; + if (done) { + this.__resetMove(); + this.viewer.resetIdleTimer(); + } + }); + } + + /** + * Triggers an event with all coordinates when a simple click is performed + */ + private __doClick(clientX: number, clientY: number, target: EventTarget, rightclick = false) { + const boundingRect = this.viewer.container.getBoundingClientRect(); + + const viewerX = clientX - boundingRect.left; + const viewerY = clientY - boundingRect.top; + + const intersections = this.viewer.renderer.getIntersections({ x: viewerX, y: viewerY }); + const sphereIntersection = intersections.find((i) => i.object.userData[VIEWER_DATA]); + + if (sphereIntersection) { + const sphericalCoords = this.viewer.dataHelper.vector3ToSphericalCoords(sphereIntersection.point); + + const data: ClickData = { + rightclick: rightclick, + target: target as HTMLElement, + clientX, + clientY, + viewerX, + viewerY, + yaw: sphericalCoords.yaw, + pitch: sphericalCoords.pitch, + objects: intersections.map((i) => i.object).filter((o) => !o.userData[VIEWER_DATA]), + }; + + try { + const textureCoords = this.viewer.dataHelper.sphericalCoordsToTextureCoords(data); + data.textureX = textureCoords.textureX; + data.textureY = textureCoords.textureY; + } catch (e) { + data.textureX = NaN; + data.textureY = NaN; + } + + if (!this.data.dblclickTimeout) { + this.viewer.dispatchEvent(new ClickEvent(positionCompat(data))); + + this.data.dblclickData = clone(data); + this.data.dblclickTimeout = setTimeout(() => { + this.data.dblclickTimeout = null; + this.data.dblclickData = null; + }, DBLCLICK_DELAY); + } else { + if ( + Math.abs(this.data.dblclickData.clientX - data.clientX) < this.moveThreshold + && Math.abs(this.data.dblclickData.clientY - data.clientY) < this.moveThreshold + ) { + this.viewer.dispatchEvent(new DoubleClickEvent(positionCompat(this.data.dblclickData))); + } + + clearTimeout(this.data.dblclickTimeout); + this.data.dblclickTimeout = null; + this.data.dblclickData = null; + } + } + } + + /** + * Trigger events for observed THREE objects + */ + private __handleObjectsEvents(evt: MouseEvent) { + if (!isEmpty(this.state.objectsObservers) && hasParent(evt.target as HTMLElement, this.viewer.container)) { + const viewerPos = getPosition(this.viewer.container); + + const viewerPoint: Point = { + x: evt.clientX - viewerPos.x, + y: evt.clientY - viewerPos.y, + }; + + const intersections = this.viewer.renderer.getIntersections(viewerPoint); + + const emit = ( + object: Mesh, + key: string, + evtCtor: new (event: MouseEvent, object: Mesh, point: Point, data: any) => ObjectEvent + ) => { + this.viewer.dispatchEvent(new evtCtor(evt, object, viewerPoint, key)); + }; + + for (const [key, object] of Object.entries(this.state.objectsObservers) as [string, Mesh | null][]) { + const intersection = intersections.find((i) => i.object.userData[key]); + + if (intersection) { + if (object && intersection.object !== object) { + emit(object, key, ObjectLeaveEvent); + this.state.objectsObservers[key] = null; + } + + if (!object) { + this.state.objectsObservers[key] = intersection.object; + emit(intersection.object, key, ObjectEnterEvent); + } else { + emit(intersection.object, key, ObjectHoverEvent); + } + } else if (object) { + emit(object, key, ObjectLeaveEvent); + this.state.objectsObservers[key] = null; + } + } + } + } + + /** + * Starts moving when crossing moveThreshold and performs movement + */ + private __doMove(clientX: number, clientY: number) { + if ( + this.data.mousedown + && (Math.abs(clientX - this.data.startMouseX) >= this.moveThreshold + || Math.abs(clientY - this.data.startMouseY) >= this.moveThreshold) + ) { + this.viewer.stopAll(); + this.__resetMove(); + this.data.step = MOVING; + this.data.mouseX = clientX; + this.data.mouseY = clientY; + this.__logMouseMove(clientX, clientY); + } else if (this.data.step === MOVING) { + this.__applyMove(clientX, clientY); + this.__logMouseMove(clientX, clientY); + } + } + + /** + * Raw method for movement, called from mouse event and move inertia + */ + private __applyMove(clientX: number, clientY: number) { + const rotation: Position = { + yaw: + this.config.moveSpeed + * ((clientX - this.data.mouseX) / this.state.size.width) + * MathUtils.degToRad(this.state.littlePlanet ? 90 : this.state.hFov), + pitch: + this.config.moveSpeed + * ((clientY - this.data.mouseY) / this.state.size.height) + * MathUtils.degToRad(this.state.littlePlanet ? 90 : this.state.vFov), + }; + + const currentPosition = this.viewer.getPosition(); + this.viewer.rotate({ + yaw: currentPosition.yaw - rotation.yaw, + pitch: currentPosition.pitch + rotation.pitch, + }); + + this.data.mouseX = clientX; + this.data.mouseY = clientY; + } + + /** + * Perfoms combined move and zoom + */ + private __doMoveZoom(evt: TouchEvent) { + if (this.data.step === MOVING) { + evt.preventDefault(); + + const p1 = { x: evt.touches[0].clientX, y: evt.touches[0].clientY }; + const p2 = { x: evt.touches[1].clientX, y: evt.touches[1].clientY }; + + const p = distance(p1, p2); + const delta = ((p - this.data.pinchDist) / SYSTEM.pixelRatio) * this.config.zoomSpeed; + + this.viewer.zoom(this.viewer.getZoomLevel() + delta); + + this.__doMove((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); + + this.data.pinchDist = p; + } + } + + /** + * Stores each mouse position during a mouse move + * @description Positions older than "INERTIA_WINDOW" are removed
+ * Positions before a pause of "INERTIA_WINDOW" / 10 are removed + */ + private __logMouseMove(clientX: number, clientY: number) { + const now = Date.now(); + + const last = this.data.mouseHistory.length + ? this.data.mouseHistory[this.data.mouseHistory.length - 1] + : [0, -1, -1]; + + // avoid duplicates + if (last[1] === clientX && last[2] === clientY) { + last[0] = now; + } else if (now === last[0]) { + last[1] = clientX; + last[2] = clientY; + } else { + this.data.mouseHistory.push([now, clientX, clientY]); + } + + let previous = null; + + for (let i = 0; i < this.data.mouseHistory.length; ) { + if (this.data.mouseHistory[i][0] < now - INERTIA_WINDOW) { + this.data.mouseHistory.splice(i, 1); + } else if (previous && this.data.mouseHistory[i][0] - previous > INERTIA_WINDOW / 10) { + this.data.mouseHistory.splice(0, i); + i = 0; + previous = this.data.mouseHistory[i][0]; + } else { + previous = this.data.mouseHistory[i][0]; + i++; + } + } + } +} diff --git a/packages/core/src/services/Renderer.ts b/packages/core/src/services/Renderer.ts new file mode 100644 index 000000000..e70e2d3dc --- /dev/null +++ b/packages/core/src/services/Renderer.ts @@ -0,0 +1,409 @@ +import { + Euler, + Group, + Intersection, + Mesh, + Object3D, + PerspectiveCamera, + Raycaster, + Renderer as ThreeRenderer, + Scene, + Vector2, + Vector3, + WebGLRenderer, +} from 'three'; +import { SPHERE_RADIUS, VIEWER_DATA } from '../data/constants'; +import { SYSTEM } from '../data/system'; +import { + BeforeRenderEvent, + ConfigChangedEvent, + PositionUpdatedEvent, + RenderEvent, + SizeUpdatedEvent, + ZoomUpdatedEvent, +} from '../events'; +import { PanoData, PanoramaOptions, Point, SphereCorrection, TextureData } from '../model'; +import { Animation, isExtendedPosition } from '../utils'; +import type { Viewer } from '../Viewer'; +import { AbstractService } from './AbstractService'; + +const vector2 = new Vector2(); + +/** + * Controller for the three.js scene + */ +export class Renderer extends AbstractService { + private readonly renderer: WebGLRenderer; + private readonly scene: Scene; + /** @internal */ + public readonly camera: PerspectiveCamera; + private readonly mesh: Mesh; + private readonly meshContainer: Group; + private readonly raycaster: Raycaster; + private readonly container: HTMLElement; + + private timestamp?: number; + private customRenderer?: ThreeRenderer; + + get panoramaPose(): Euler { + return this.mesh.rotation; + } + + get sphereCorrection(): Euler { + return this.meshContainer.rotation; + } + + /** + * @internal + */ + constructor(viewer: Viewer) { + super(viewer); + + this.renderer = new WebGLRenderer({ alpha: true, antialias: true }); + this.renderer.setPixelRatio(SYSTEM.pixelRatio); + this.renderer.domElement.className = 'psv-canvas'; + + this.scene = new Scene(); + + this.camera = new PerspectiveCamera(50, 16 / 9, 0.1, 2 * SPHERE_RADIUS); + + this.mesh = this.viewer.adapter.createMesh(); + this.mesh.userData = { [VIEWER_DATA]: true }; + + this.meshContainer = new Group(); + this.meshContainer.add(this.mesh); + this.scene.add(this.meshContainer); + + this.raycaster = new Raycaster(); + + this.container = document.createElement('div'); + this.container.className = 'psv-canvas-container'; + this.container.style.background = this.config.canvasBackground; + this.container.appendChild(this.renderer.domElement); + this.viewer.container.appendChild(this.container); + + this.viewer.addEventListener(SizeUpdatedEvent.type, this); + this.viewer.addEventListener(ZoomUpdatedEvent.type, this); + this.viewer.addEventListener(PositionUpdatedEvent.type, this); + this.viewer.addEventListener(ConfigChangedEvent.type, this); + + this.hide(); + } + + /** + * @internal + */ + init() { + if (this.config.mousemove) { + this.container.style.cursor = 'move'; + } + this.show(); + this.renderer.setAnimationLoop((t) => this.__renderLoop(t)); + } + + /** + * @internal + */ + override destroy() { + // cancel render loop + this.renderer.setAnimationLoop(null); + + // destroy ThreeJS view + this.cleanScene(this.scene); + + // remove container + this.viewer.container.removeChild(this.container); + + this.viewer.removeEventListener(SizeUpdatedEvent.type, this); + this.viewer.removeEventListener(ZoomUpdatedEvent.type, this); + this.viewer.removeEventListener(PositionUpdatedEvent.type, this); + this.viewer.removeEventListener(ConfigChangedEvent.type, this); + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + // prettier-ignore + switch (e.type) { + case SizeUpdatedEvent.type: this.__onSizeUpdated(); break; + case ZoomUpdatedEvent.type: this.__onZoomUpdated(); break; + case PositionUpdatedEvent.type: this.__onPositionUpdated(); break; + case ConfigChangedEvent.type: + if ((e as ConfigChangedEvent).containsOptions('fisheye')) { + this.__onPositionUpdated(); + } + if ((e as ConfigChangedEvent).containsOptions('mousemove')) { + this.container.style.cursor = this.config.mousemove ? 'move' : 'default'; + } + if ((e as ConfigChangedEvent).containsOptions('canvasBackground')) { + this.container.style.background = this.config.canvasBackground; + } + break; + } + } + + /** + * Hides the viewer + */ + hide() { + this.container.style.opacity = '0'; + } + + /** + * Shows the viewer + */ + show() { + this.container.style.opacity = '1'; + } + + /** + * Resets or replaces the THREE renderer by a custom one + */ + setCustomRenderer(factory: (renderer: WebGLRenderer) => ThreeRenderer) { + if (factory) { + this.customRenderer = factory(this.renderer); + } else { + this.customRenderer = null; + } + this.viewer.needsUpdate(); + } + + /** + * Updates the size of the renderer and the aspect of the camera + */ + private __onSizeUpdated() { + this.renderer.setSize(this.state.size.width, this.state.size.height); + this.camera.aspect = this.state.aspect; + this.camera.updateProjectionMatrix(); + this.viewer.needsUpdate(); + } + + /** + * Updates the fov of the camera + */ + private __onZoomUpdated() { + this.camera.fov = this.state.vFov; + this.camera.updateProjectionMatrix(); + this.viewer.needsUpdate(); + } + + /** + * Updates the position of the camera + */ + private __onPositionUpdated() { + this.camera.position.set(0, 0, 0); + this.camera.lookAt(this.state.direction); + if (this.config.fisheye) { + this.camera.position + .copy(this.state.direction) + .multiplyScalar(this.config.fisheye / 2) + .negate(); + } + this.viewer.needsUpdate(); + } + + /** + * Main event loop, performs a render if `state.needsUpdate` is true + */ + private __renderLoop(timestamp: number) { + const elapsed = !this.timestamp ? 0 : timestamp - this.timestamp; + this.timestamp = timestamp; + + this.viewer.dispatchEvent(new BeforeRenderEvent(timestamp, elapsed)); + this.viewer.dynamics.update(elapsed); + + if (this.state.needsUpdate) { + (this.customRenderer || this.renderer).render(this.scene, this.camera); + this.viewer.dispatchEvent(new RenderEvent()); + this.state.needsUpdate = false; + } + } + + /** + * Applies the texture to the scene, creates the scene if needed + * @internal + */ + setTexture(textureData: TextureData) { + this.state.panoData = textureData.panoData; + + this.viewer.adapter.setTexture(this.mesh, textureData); + + this.viewer.needsUpdate(); + } + + /** + * Applies the overlay to the mesh + * @internal + */ + setOverlay(textureData: TextureData, opacity: number) { + this.viewer.adapter.setOverlay(this.mesh, textureData, opacity); + this.viewer.needsUpdate(); + } + + /** + * Applies a panorama data pose to a Mesh + * @internal + */ + setPanoramaPose(panoData: PanoData, mesh: Mesh = this.mesh) { + // By Google documentation the angles are applied on the camera in order : heading, pitch, roll + // here we apply the reverse transformation on the sphere + const cleanCorrection = this.viewer.dataHelper.cleanPanoramaPose(panoData); + + mesh.rotation.set(-cleanCorrection.tilt, -cleanCorrection.pan, -cleanCorrection.roll, 'ZXY'); + } + + /** + * Applies a SphereCorrection to a Group + * @internal + */ + setSphereCorrection(sphereCorrection: SphereCorrection, group: Group = this.meshContainer) { + const cleanCorrection = this.viewer.dataHelper.cleanSphereCorrection(sphereCorrection); + + group.rotation.set(cleanCorrection.tilt, cleanCorrection.pan, cleanCorrection.roll, 'ZXY'); + } + + /** + * Performs transition between the current and a new texture + * @internal + */ + transition(textureData: TextureData, options: PanoramaOptions): Animation { + const positionProvided = isExtendedPosition(options); + const zoomProvided = 'zoom' in options; + + // create temp group and new mesh, half size to be in "front" of the first one + const group = new Group(); + const mesh = this.viewer.adapter.createMesh(0.5); + this.viewer.adapter.setTexture(mesh, textureData, true); + this.viewer.adapter.setTextureOpacity(mesh, 0); + this.setPanoramaPose(textureData.panoData, mesh); + this.setSphereCorrection(options.sphereCorrection, group); + + // rotate the new sphere to make the target position face the camera + if (positionProvided) { + const cleanPosition = this.viewer.dataHelper.cleanPosition(options); + const currentPosition = this.viewer.getPosition(); + + // rotation along the vertical axis + const verticalAxis = new Vector3(0, 1, 0); + group.rotateOnWorldAxis(verticalAxis, cleanPosition.yaw - currentPosition.yaw); + + // rotation along the camera horizontal axis + const horizontalAxis = new Vector3(0, 1, 0).cross(this.camera.getWorldDirection(new Vector3())).normalize(); + group.rotateOnWorldAxis(horizontalAxis, cleanPosition.pitch - currentPosition.pitch); + } + + group.add(mesh); + this.scene.add(group); + + const animation = new Animation({ + properties: { + opacity: { start: 0.0, end: 1.0 }, + zoom: zoomProvided ? { start: this.viewer.getZoomLevel(), end: options.zoom } : undefined, + }, + duration: options.transition as number, + easing: 'outCubic', + onTick: (properties) => { + this.viewer.adapter.setTextureOpacity(mesh, properties.opacity); + this.viewer.adapter.setTextureOpacity(this.mesh, 1 - properties.opacity); + + if (zoomProvided) { + this.viewer.zoom(properties.zoom); + } + + this.viewer.needsUpdate(); + }, + }); + + animation.then((completed) => { + if (completed) { + // remove temp sphere and transfer the texture to the main mesh + this.setTexture(textureData); + this.viewer.adapter.setTextureOpacity(this.mesh, 1); + this.setPanoramaPose(textureData.panoData); + this.setSphereCorrection(options.sphereCorrection); + + // actually rotate the camera + if (positionProvided) { + this.viewer.rotate(options); + } + } else { + this.viewer.adapter.disposeTexture(textureData); + } + + this.scene.remove(group); + mesh.geometry.dispose(); + mesh.geometry = null; + }); + + return animation; + } + + /** + * Returns intersections with objects in the scene + */ + getIntersections(viewerPoint: Point): Intersection[] { + vector2.x = (2 * viewerPoint.x) / this.state.size.width - 1; + vector2.y = (-2 * viewerPoint.y) / this.state.size.height + 1; + + this.raycaster.setFromCamera(vector2, this.camera); + + return this.raycaster + .intersectObjects(this.scene.children, true) + .filter((i) => (i.object as Mesh).isMesh && !!i.object.userData) as Intersection[]; + } + + /** + * Adds an object to the THREE scene + */ + addObject(object: Object3D) { + this.scene.add(object); + } + + /** + * Removes an object from the THREE scene + */ + removeObject(object: Object3D) { + this.scene.remove(object); + } + + /** + * Calls `dispose` on all objects and textures + * @internal + */ + cleanScene(object: any) { + object.traverse((item: any) => { + if (item.geometry) { + item.geometry.dispose(); + } + + if (item.material) { + if (Array.isArray(item.material)) { + item.material.forEach((material: any) => { + if (material.map) { + material.map.dispose(); + } + + material.dispose(); + }); + } else { + if (item.material.map) { + item.material.map.dispose(); + } + + item.material.dispose(); + } + } + + if (item.dispose && !(item instanceof Scene)) { + item.dispose(); + } + + if (item !== object) { + this.cleanScene(item); + } + }); + } +} diff --git a/packages/core/src/services/TextureLoader.ts b/packages/core/src/services/TextureLoader.ts new file mode 100644 index 000000000..edb88bd94 --- /dev/null +++ b/packages/core/src/services/TextureLoader.ts @@ -0,0 +1,104 @@ +import { FileLoader } from 'three'; +import { PSVError } from '../PSVError'; +import type { Viewer } from '../Viewer'; +import { AbstractService } from './AbstractService'; + +/** + * Image and texture loading system + */ +export class TextureLoader extends AbstractService { + private readonly loader: FileLoader; + + /** + * @internal + */ + constructor(viewer: Viewer) { + super(viewer); + + this.loader = new FileLoader(); + this.loader.setResponseType('blob'); + if (this.config.withCredentials) { + this.loader.setWithCredentials(true); + } + } + + /** + * @internal + */ + override destroy() { + this.abortLoading(); + super.destroy(); + } + + /** + * Cancels current HTTP requests + * @internal + */ + abortLoading() { + // noop implementation waiting for https://github.com/mrdoob/three.js/pull/23070 + } + + /** + * Loads a Blob with FileLoader + */ + loadFile(url: string, onProgress?: (p: number) => void): Promise { + if (this.config.requestHeaders) { + this.loader.setRequestHeader(this.config.requestHeaders(url)); + } + + return new Promise((resolve, reject) => { + let progress = 0; + onProgress?.(progress); + + this.loader.load( + url, + (result) => { + progress = 100; + onProgress?.(progress); + resolve(result as any as Blob); + }, + (e) => { + if (e.lengthComputable) { + const newProgress = (e.loaded / e.total) * 100; + if (newProgress > progress) { + progress = newProgress; + onProgress?.(progress); + } + } + }, + (err) => { + reject(err); + } + ); + }); + } + + /** + * Loads an Image using FileLoader to have progress events + */ + loadImage(url: string, onProgress?: (p: number) => void): Promise { + return this.loadFile(url, onProgress).then( + (result) => + new Promise((resolve, reject) => { + const img = document.createElement('img'); + img.onload = () => { + URL.revokeObjectURL(img.src); + resolve(img); + }; + img.onerror = reject; + img.src = URL.createObjectURL(result); + }) + ); + } + + /** + * Preload a panorama file without displaying it + */ + preloadPanorama(panorama: any): Promise { + if (this.viewer.adapter.supportsPreload(panorama)) { + return this.viewer.adapter.loadTexture(panorama); + } else { + return Promise.reject(new PSVError('Current adapter does not support preload')); + } + } +} diff --git a/packages/core/src/services/ViewerDynamics.ts b/packages/core/src/services/ViewerDynamics.ts new file mode 100644 index 000000000..15702c0fe --- /dev/null +++ b/packages/core/src/services/ViewerDynamics.ts @@ -0,0 +1,66 @@ +import { MathUtils } from 'three'; +import { Dynamic, MultiDynamic, positionCompat } from '../utils'; +import type { Viewer } from '../Viewer'; +import { PositionUpdatedEvent, ZoomUpdatedEvent } from '../events'; +import { AbstractService } from './AbstractService'; + +export class ViewerDynamics extends AbstractService { + readonly zoom = new Dynamic( + (zoomLevel) => { + this.viewer.state.vFov = this.viewer.dataHelper.zoomLevelToFov(zoomLevel); + this.viewer.state.hFov = this.viewer.dataHelper.vFovToHFov(this.viewer.state.vFov); + this.viewer.dispatchEvent(new ZoomUpdatedEvent(zoomLevel)); + }, + { + defaultValue: this.viewer.config.defaultZoomLvl, + min: 0, + max: 100, + wrap: false, + } + ); + + readonly position = new MultiDynamic( + (position) => { + this.viewer.dataHelper.sphericalCoordsToVector3(position, this.viewer.state.direction); + this.viewer.dispatchEvent(new PositionUpdatedEvent(positionCompat(position))); + }, + { + yaw: new Dynamic(null, { + defaultValue: this.config.defaultYaw, + min: 0, + max: 2 * Math.PI, + wrap: true, + }), + pitch: new Dynamic(null, { + defaultValue: this.config.defaultPitch, + min: !this.viewer.state.littlePlanet ? -Math.PI / 2 : 0, + max: !this.viewer.state.littlePlanet ? Math.PI / 2 : Math.PI * 2, + wrap: this.viewer.state.littlePlanet, + }), + } + ); + + /** + * @internal + */ + constructor(viewer: Viewer) { + super(viewer); + this.updateSpeeds(); + } + + /** + * @internal + */ + updateSpeeds() { + this.zoom.setSpeed(this.config.zoomSpeed * 50); + this.position.setSpeed(MathUtils.degToRad(this.config.moveSpeed * 50)); + } + + /** + * @internal + */ + update(elapsed: number) { + this.zoom.update(elapsed); + this.position.update(elapsed); + } +} diff --git a/packages/core/src/services/ViewerState.ts b/packages/core/src/services/ViewerState.ts new file mode 100644 index 000000000..42c3fe5ff --- /dev/null +++ b/packages/core/src/services/ViewerState.ts @@ -0,0 +1,103 @@ +import { Mesh, Vector3 } from 'three'; +import { SPHERE_RADIUS } from '../data/constants'; +import { PanoData, Size } from '../model'; +import type { Animation } from '../utils'; + +/** + * Internal properties of the viewer + */ +export class ViewerState { + /** + * when all components are loaded + */ + ready = false; + + /** + * if the view needs to be renderer + */ + needsUpdate = false; + + /** + * if the keyboard events are currently listened to + */ + keyboardEnabled = false; + + /** + * direction of the camera + */ + direction = new Vector3(0, 0, SPHERE_RADIUS); + + /** + * vertical FOV + */ + vFov = 60; + + /** + * horizontal FOV + */ + hFov = 60; + + /** + * renderer aspect ratio + */ + aspect = 1; + + /** + * currently running animation + */ + animation: Animation = null; + + /** + * currently running transition + */ + transitionAnimation: Animation = null; + + /** + * promise of the last "setPanorama()" call + */ + loadingPromise: Promise = null; + + /** + * special tweaks for LittlePlanetAdapter + */ + littlePlanet = false; + + /** + * time of the last user action + */ + idleTime = -1; + + /** + * registered THREE objects observer + */ + objectsObservers: Record = {}; + + /** + * size of the container + */ + size: Size = { + width: 0, + height: 0, + }; + + /** + * panorama metadata, if supported + */ + panoData: PanoData = { + fullWidth: 0, + fullHeight: 0, + croppedWidth: 0, + croppedHeight: 0, + croppedX: 0, + croppedY: 0, + poseHeading: 0, + posePitch: 0, + poseRoll: 0, + }; + + /** + * @internal + */ + // eslint-disable-next-line @typescript-eslint/no-empty-function + constructor() {} +} diff --git a/src/styles/index.scss b/packages/core/src/styles/index.scss similarity index 58% rename from src/styles/index.scss rename to packages/core/src/styles/index.scss index 5a11847d2..0a5dca036 100644 --- a/src/styles/index.scss +++ b/packages/core/src/styles/index.scss @@ -1,8 +1,10 @@ +@use 'sass:list'; +@use 'sass:map'; +@import '../../../shared/src/vars'; @import 'viewer'; @import 'loader'; @import 'navbar'; -@import 'buttons/autorotate'; -@import 'buttons/zoom-range'; +@import 'zoom-range'; @import 'notification'; @import 'overlay'; @import 'panel'; diff --git a/packages/core/src/styles/loader.scss b/packages/core/src/styles/loader.scss new file mode 100644 index 000000000..d182fb119 --- /dev/null +++ b/packages/core/src/styles/loader.scss @@ -0,0 +1,37 @@ +.psv-loader-container { + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: $psv-loader-zindex; +} + +.psv-loader { + position: relative; + display: flex; + justify-content: center; + align-items: center; + color: $psv-loader-color; + width: $psv-loader-width; + height: $psv-loader-width; + outline: $psv-loader-border solid transparent; + + &-canvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + color: $psv-loader-bg-color; + outline: $psv-loader-tickness solid transparent; + z-index: -1; + } + + &-text { + font: $psv-loader-font; + } +} diff --git a/packages/core/src/styles/navbar.scss b/packages/core/src/styles/navbar.scss new file mode 100644 index 000000000..ac8836804 --- /dev/null +++ b/packages/core/src/styles/navbar.scss @@ -0,0 +1,82 @@ +.psv-navbar { + display: flex; + position: absolute; + z-index: $psv-navbar-zindex; + bottom: -$psv-navbar-height; + left: 0; + width: 100%; + height: $psv-navbar-height; + background: $psv-navbar-background; + transition: bottom ease-in-out 0.1s; + + &--open { + bottom: 0; + } + + &, + & * { + box-sizing: content-box; + } +} + +.psv-button { + flex: 0 0 auto; + padding: $psv-buttons-padding; + position: relative; + cursor: pointer; + height: $psv-buttons-height; + width: $psv-buttons-height; + background: $psv-buttons-background; + color: $psv-buttons-color; + + &--active { + background: $psv-buttons-active-background; + } + + &--disabled { + pointer-events: none; + opacity: $psv-buttons-disabled-opacity; + } + + &-svg { + width: 100%; + transform: scale(1); + transition: transform $psv-buttons-hover-scale-delay ease; + } +} + +.psv-button:not(.psv-button--disabled):focus-visible { + outline: $psv-element-focus-outline; + outline-offset: -#{list.nth($psv-element-focus-outline, 1)}; +} + +.psv-container:not(.psv--is-touch) .psv-button--hover-scale:not(.psv-button--disabled):hover .psv-button-svg { + transform: scale($psv-buttons-hover-scale); +} + +.psv-move-button + .psv-move-button { + margin-left: -$psv-buttons-padding; +} + +.psv-custom-button { + width: auto; + min-width: $psv-buttons-height; +} + +.psv-caption { + flex: 1 1 100%; + color: $psv-caption-text-color; + overflow: hidden; + text-align: center; + cursor: default; + padding: unset; + height: unset; + width: unset; + + &-content { + display: inline-block; + padding: $psv-buttons-padding; + font: $psv-caption-font; + white-space: nowrap; + } +} diff --git a/packages/core/src/styles/notification.scss b/packages/core/src/styles/notification.scss new file mode 100644 index 000000000..c3adacfa4 --- /dev/null +++ b/packages/core/src/styles/notification.scss @@ -0,0 +1,28 @@ +.psv-notification { + position: absolute; + z-index: $psv-notification-zindex; + bottom: $psv-notification-position-from; + display: flex; + justify-content: center; + box-sizing: border-box; + width: 100%; + padding: 0 2em; + opacity: 0; + transition-property: opacity, bottom; + transition-timing-function: ease-in-out; + transition-duration: $psv-notification-animate-delay; + + &-content { + max-width: 50em; + background: $psv-notification-background; + border-radius: $psv-notification-radius; + padding: $psv-notification-padding; + font: $psv-notification-font; + color: $psv-notification-text-color; + } + + &--visible { + opacity: 100; + bottom: $psv-notification-position-to; + } +} diff --git a/packages/core/src/styles/overlay.scss b/packages/core/src/styles/overlay.scss new file mode 100644 index 000000000..9ae4fe9af --- /dev/null +++ b/packages/core/src/styles/overlay.scss @@ -0,0 +1,39 @@ +.psv-overlay { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: absolute; + z-index: $psv-overlay-zindex; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: $psv-main-background; + opacity: $psv-overlay-opacity; + + &-image { + margin-bottom: 4vh; + + svg { + width: map.get($psv-overlay-image-size, portrait); + + @media (orientation: landscape) { + width: map.get($psv-overlay-image-size, landscape); + } + } + } + + &-title { + color: $psv-overlay-title-color; + font: $psv-overlay-title-font; + text-align: center; + } + + &-text { + color: $psv-overlay-text-color; + font: $psv-overlay-text-font; + opacity: 0.8; + text-align: center; + } +} diff --git a/packages/core/src/styles/panel.scss b/packages/core/src/styles/panel.scss new file mode 100644 index 000000000..d286d261e --- /dev/null +++ b/packages/core/src/styles/panel.scss @@ -0,0 +1,237 @@ +@function make-dot-shadow($color, $w, $h) { + $val: 1px 0 $color; + $x: 3; + $y: 0; + + @while $y < $h { + @if $x > $w { + $x: 1; + $y: $y + 2; + } @else { + $val: #{$val}, #{$x}px #{$y}px #{$color}; + $x: $x + 2; + } + } + + @return $val; +} + +.psv-panel { + position: absolute; + z-index: $psv-panel-zindex; + right: 0; + height: 100%; + width: $psv-panel-width; + max-width: calc(100% - #{$psv-panel-resizer-width}); + background: $psv-panel-background; + transform: translate3d(100%, 0, 0); + opacity: 0; + transition-property: opacity, transform; + transition-timing-function: ease-in-out; + transition-duration: $psv-panel-animate-delay; + cursor: default; + margin-left: $psv-panel-resizer-width; + + .psv--has-navbar & { + height: calc(100% - #{$psv-navbar-height}); + } + + &-close-button { + display: none; + position: absolute; + top: -1px; + right: 0; + width: $psv-panel-close-button-size; + height: $psv-panel-close-button-size; + background: transparent; + transition: background $psv-panel-close-button-animate-delay ease-in-out; + cursor: pointer; + + &::before, + &::after { + content: ''; + position: absolute; + top: 50%; + left: $psv-panel-close-button-size * 0.125; + width: $psv-panel-close-button-size * 0.75; + height: 2px; + background-color: $psv-panel-close-button-color; + transition: $psv-panel-close-button-animate-delay ease-in-out; + transition-property: width, left, transform; + } + + &::before { + transform: rotate(45deg); + } + + &::after { + transform: rotate(-45deg); + } + + &:hover { + background: $psv-panel-close-button-background; + + &::before { + transform: rotate(45deg) scaleX(-1); + } + + &::after { + transform: rotate(-45deg) scaleX(-1); + } + } + } + + &-resizer { + display: none; + position: absolute; + top: 0; + left: -$psv-panel-resizer-width; + width: $psv-panel-resizer-width; + height: 100%; + background-color: $psv-panel-resizer-background; + cursor: col-resize; + + $psv-panel-resizer-grip-width: $psv-panel-resizer-width - 4px; + + @if $psv-panel-resizer-grip-width > 0 { + &::before { + content: ''; + position: absolute; + top: 50%; + left: ($psv-panel-resizer-width - $psv-panel-resizer-grip-width) * 0.5 - 1px; + margin-top: (-$psv-panel-resizer-grip-height * 0.5); + width: 1px; + height: 1px; + box-shadow: make-dot-shadow( + $psv-panel-resizer-grip-color, + $psv-panel-resizer-grip-width, + $psv-panel-resizer-grip-height + ); + background: transparent; + } + } + } + + &-content { + width: 100%; + height: 100%; + box-sizing: border-box; + color: $psv-panel-text-color; + font: $psv-panel-font; + overflow: auto; + + &:not(&--no-margin) { + padding: $psv-panel-padding; + } + + &--no-interaction { + user-select: none; + pointer-events: none; + } + } + + &--open { + transform: translate3d(0, 0, 0); + opacity: 1; + transition-duration: 0.2s; + + .psv-panel-close-button, + .psv-panel-resizer { + display: block; + } + } + + @media screen and (max-width: #{$psv-panel-width}) { + width: 100% !important; + max-width: none; + + &-resizer { + display: none !important; + } + } +} + +.psv-panel-menu { + height: 100%; + display: flex; + flex-direction: column; + + &-title { + flex: none; + display: flex; + align-items: center; + font: $psv-panel-title-font; + margin: $psv-panel-title-margin $psv-panel-title-margin * 0.5; + + svg { + width: $psv-panel-title-icon-size; + height: $psv-panel-title-icon-size; + margin-right: $psv-panel-title-margin * 0.5; + } + } + + &-list { + flex: 1; + list-style: none; + margin: 0; + padding: 0; + overflow-x: hidden; + } + + &-item { + min-height: $psv-panel-menu-item-height; + padding: $psv-panel-menu-item-padding; + cursor: pointer; + display: flex; + align-items: center; + justify-content: flex-start; + transition: background 0.1s ease-in-out; + + &--active { + outline: $psv-panel-menu-item-active-outline solid currentcolor; + outline-offset: -$psv-panel-menu-item-active-outline; + } + + &-icon { + flex: none; + height: $psv-panel-menu-item-height; + width: $psv-panel-menu-item-height; + margin-right: #{list.nth($psv-panel-menu-item-padding, 1)}; + + img { + max-width: 100%; + max-height: 100%; + } + + svg { + width: 100%; + height: 100%; + } + } + + &:focus-visible { + outline: $psv-element-focus-outline; + outline-offset: -#{list.nth($psv-element-focus-outline, 1)}; + } + } + + &--stripped &-item { + &:hover { + background: $psv-panel-menu-hover-background; + } + + &:nth-child(odd), + &:nth-child(odd)::before { + background: $psv-panel-menu-odd-background; + } + + &:nth-child(even), + &:nth-child(even)::before { + background: $psv-panel-menu-even-background; + } + } +} + +.psv-container:not(.psv--is-touch) .psv-panel-menu-item:hover { + background: $psv-panel-menu-hover-background; +} diff --git a/packages/core/src/styles/tooltip.scss b/packages/core/src/styles/tooltip.scss new file mode 100644 index 000000000..3f9701f85 --- /dev/null +++ b/packages/core/src/styles/tooltip.scss @@ -0,0 +1,108 @@ +.psv-tooltip { + position: absolute; + z-index: $psv-tooltip-zindex; + box-sizing: border-box; + max-width: $psv-tooltip-max-width; + background: $psv-tooltip-background; + border-radius: $psv-tooltip-radius; + padding: $psv-tooltip-padding; + opacity: 0; + transition-property: opacity, transform; + transition-timing-function: ease-in-out; + transition-duration: $psv-tooltip-animate-delay; + + &-content { + color: $psv-tooltip-text-color; + font: $psv-tooltip-font; + text-shadow: $psv-tooltip-text-shadow; + } + + &-arrow { + position: absolute; + height: 0; + width: 0; + border: $psv-tooltip-arrow-size solid transparent; + } + + &--top-left, + &--top-center, + &--top-right { + transform: translate3d(0, $psv-tooltip-animate-offset, 0); + + .psv-tooltip-arrow { + border-top-color: $psv-tooltip-arrow-color; + } + } + + &--bottom-left, + &--bottom-center, + &--bottom-right { + transform: translate3d(0, -$psv-tooltip-animate-offset, 0); + + .psv-tooltip-arrow { + border-bottom-color: $psv-tooltip-arrow-color; + } + } + + &--left-top, + &--center-left, + &--left-bottom { + transform: translate3d($psv-tooltip-animate-offset, 0, 0); + + .psv-tooltip-arrow { + border-left-color: $psv-tooltip-arrow-color; + } + } + + &--right-top, + &--center-right, + &--right-bottom { + transform: translate3d(-$psv-tooltip-animate-offset, 0, 0); + + .psv-tooltip-arrow { + border-right-color: $psv-tooltip-arrow-color; + } + } + + &--left-top, + &--top-left { + box-shadow: #{-$psv-tooltip-shadow-offset} #{-$psv-tooltip-shadow-offset} 0 $psv-tooltip-shadow-color; + } + + &--top-center { + box-shadow: 0 #{-$psv-tooltip-shadow-offset} 0 $psv-tooltip-shadow-color; + } + + &--right-top, + &--top-right { + box-shadow: $psv-tooltip-shadow-offset #{-$psv-tooltip-shadow-offset} 0 $psv-tooltip-shadow-color; + } + + &--left-bottom, + &--bottom-left { + box-shadow: #{-$psv-tooltip-shadow-offset} $psv-tooltip-shadow-offset 0 $psv-tooltip-shadow-color; + } + + &--bottom-center { + box-shadow: 0 $psv-tooltip-shadow-offset 0 $psv-tooltip-shadow-color; + } + + &--right-bottom, + &--bottom-right { + box-shadow: $psv-tooltip-shadow-offset $psv-tooltip-shadow-offset 0 $psv-tooltip-shadow-color; + } + + &--center-left { + box-shadow: #{-$psv-tooltip-shadow-offset} 0 0 $psv-tooltip-shadow-color; + } + + &--center-right { + box-shadow: $psv-tooltip-shadow-offset 0 0 $psv-tooltip-shadow-color; + } + + &--visible { + transform: translate3d(0, 0, 0); + opacity: 1; + transition-duration: $psv-tooltip-animate-delay; + } +} diff --git a/packages/core/src/styles/viewer.scss b/packages/core/src/styles/viewer.scss new file mode 100644 index 000000000..02117abc8 --- /dev/null +++ b/packages/core/src/styles/viewer.scss @@ -0,0 +1,21 @@ +.psv-container { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + position: relative; + background: $psv-main-background; + overflow: hidden; +} + +.psv-canvas-container { + position: absolute; + top: 0; + left: 0; + z-index: $psv-canvas-zindex; + transition: opacity linear 100ms; +} + +.psv-canvas { + display: block; +} diff --git a/packages/core/src/styles/zoom-range.scss b/packages/core/src/styles/zoom-range.scss new file mode 100644 index 000000000..10088b78b --- /dev/null +++ b/packages/core/src/styles/zoom-range.scss @@ -0,0 +1,38 @@ +.psv-zoom-range { + &.psv-button { + width: $psv-zoom-range-width; + height: $psv-zoom-range-tickness; + margin: $psv-buttons-padding 0; + padding: #{($psv-buttons-height - $psv-zoom-range-tickness) * 0.5} 0; + max-width: $psv-zoom-range-media-min-width; // trick for JS access + } + + &-line { + position: relative; + width: $psv-zoom-range-width; + height: $psv-zoom-range-tickness; + background: $psv-buttons-color; + transition: all 0.3s ease; + } + + &-handle { + position: absolute; + border-radius: 50%; + top: #{($psv-zoom-range-tickness - $psv-zoom-range-diameter) * 0.5}; + width: $psv-zoom-range-diameter; + height: $psv-zoom-range-diameter; + background: $psv-buttons-color; + transform: scale(1); + transition: transform 0.3s ease; + } + + &:not(.psv-button--disabled):hover { + .psv-zoom-range-line { + box-shadow: 0 0 2px $psv-buttons-color; + } + + .psv-zoom-range-handle { + transform: scale(1.3); + } + } +} diff --git a/packages/core/src/utils/Animation.ts b/packages/core/src/utils/Animation.ts new file mode 100644 index 000000000..bf864c812 --- /dev/null +++ b/packages/core/src/utils/Animation.ts @@ -0,0 +1,172 @@ +import { EASINGS } from '../data/constants'; + +/** + * Options for {@link Animation} + */ +export type AnimationOptions = { + /** + * interpolated properties + */ + properties: Partial>; + /** + * duration of the animation + */ + duration: number; + /** + * delay before start + * @default 0 + */ + delay?: number; + /** + * interpoaltion function, see {@link CONSTANTS.EASINGS} + * @default 'linear' + */ + easing?: string | ((t: number) => number); + /** + * function called for each frame + */ + onTick: (properties: Record, progress: number) => void; +}; + +type PropertyValues = AnimationOptions['properties']['k']; + +/** + * @summary Interpolation helper for animations + * @description + * Implements the Promise API with an additional "cancel" method. + * The promise is resolved with `true` when the animation is completed and `false` if the animation is cancelled. + * @template T the type of interpoalted properties + * + * @example + * ```ts + * const anim = new Animation({ + * properties: { + * width: {start: 100, end: 200} + * }, + * duration: 5000, + * onTick: (properties) => element.style.width = `${properties.width}px`; + * }); + * + * anim.then((completed) => ...); + * + * anim.cancel(); + * ``` + */ +export class Animation implements PromiseLike { + private options: AnimationOptions; + private easing: (t: number) => number = EASINGS['linear']; + private callbacks: ((complete: boolean) => void)[] = []; + private start?: number; + private delayTimeout: ReturnType; + private animationFrame: ReturnType; + + resolved = false; + cancelled = false; + + constructor(options: AnimationOptions) { + this.options = options; + + if (options) { + if (options.easing) { + this.easing = + typeof options.easing === 'function' + ? options.easing + : EASINGS[options.easing] || EASINGS['linear']; + } + + if (options.delay) { + this.delayTimeout = setTimeout(() => { + this.delayTimeout = undefined; + this.animationFrame = window.requestAnimationFrame((t) => this.__run(t)); + }, options.delay); + } else { + this.animationFrame = window.requestAnimationFrame((t) => this.__run(t)); + } + } else { + this.resolved = true; + } + } + + private __run(timestamp: number) { + if (this.cancelled) { + return; + } + + // first iteration + if (!this.start) { + this.start = timestamp; + } + + // compute progress + const progress = (timestamp - this.start) / this.options.duration; + const current = {} as Record; + + if (progress < 1.0) { + // interpolate properties + for (const [name, prop] of Object.entries(this.options.properties) as [string, PropertyValues][]) { + if (prop) { + const value = prop.start + (prop.end - prop.start) * this.easing(progress); + // @ts-ignore + current[name] = value; + } + } + this.options.onTick(current, progress); + + this.animationFrame = window.requestAnimationFrame((t) => this.__run(t)); + } else { + // call onTick one last time with final values + for (const [name, prop] of Object.entries(this.options.properties) as [string, PropertyValues][]) { + if (prop) { + // @ts-ignore + current[name] = prop.end; + } + } + this.options.onTick(current, 1.0); + + this.__resolve(true); + this.animationFrame = undefined; + } + } + + private __resolve(value: boolean) { + if (value) { + this.resolved = true; + } else { + this.cancelled = true; + } + this.callbacks.forEach((cb) => cb(value)); + this.callbacks.length = 0; + } + + /** + * Promise chaining + * @param [onFulfilled] - Called when the animation is complete (true) or cancelled (false) + */ + then(onFulfilled: (complete: boolean) => PromiseLike | T): Promise { + if (this.resolved || this.cancelled) { + return Promise.resolve(this.resolved).then(onFulfilled); + } + + return new Promise((resolve: (complete: boolean) => void) => { + this.callbacks.push(resolve); + }).then(onFulfilled); + } + + /** + * Cancels the animation + */ + cancel() { + if (!this.cancelled && !this.resolved) { + this.__resolve(false); + + if (this.delayTimeout) { + window.clearTimeout(this.delayTimeout); + this.delayTimeout = undefined; + } + if (this.animationFrame) { + window.cancelAnimationFrame(this.animationFrame); + this.animationFrame = undefined; + } + } + } +} diff --git a/packages/core/src/utils/Dynamic.ts b/packages/core/src/utils/Dynamic.ts new file mode 100644 index 000000000..1ddf10465 --- /dev/null +++ b/packages/core/src/utils/Dynamic.ts @@ -0,0 +1,173 @@ +import { MathUtils } from 'three'; +import { PSVError } from '../PSVError'; +import { wrap } from './math'; + +const enum DynamicMode { + STOP, + INFINITE, + POSITION, +} + +/** + * Represents a variable that can dynamically change with time (using requestAnimationFrame) + */ +export class Dynamic { + private readonly min: number; + private readonly max: number; + private readonly wrap: boolean; + + private mode = DynamicMode.STOP; + private speed = 0; + private speedMult = 0; + private currentSpeed = 0; + private target = 0; + private __current = 0; + + get current(): number { + return this.__current; + } + + private set current(current: number) { + this.__current = current; + } + + constructor( + private readonly fn: (val: number) => void, + config: { + min: number; + max: number; + defaultValue: number; + wrap: boolean; + } + ) { + this.min = config.min; + this.max = config.max; + this.wrap = config.wrap; + this.current = config.defaultValue; + + if (this.wrap && this.min !== 0) { + throw new PSVError('invalid config'); + } + + if (this.fn) { + this.fn(this.current); + } + } + + /** + * Changes base speed + */ + setSpeed(speed: number) { + this.speed = speed; + } + + /** + * Defines the target position + */ + goto(position: number, speedMult = 1) { + this.mode = DynamicMode.POSITION; + this.target = this.wrap ? wrap(position, this.max) : MathUtils.clamp(position, this.min, this.max); + this.speedMult = speedMult; + } + + /** + * Increases/decreases the target position + */ + step(step: number, speedMult = 1) { + if (this.mode !== DynamicMode.POSITION) { + this.target = this.current; + } + this.goto(this.target + step, speedMult); + } + + /** + * Starts infinite movement + */ + roll(invert = false, speedMult = 1) { + this.mode = DynamicMode.INFINITE; + this.target = invert ? -Infinity : Infinity; + this.speedMult = speedMult; + } + + /** + * Stops movement + */ + stop() { + this.mode = DynamicMode.STOP; + } + + /** + * Defines the current position and immediately stops movement + * @param {number} value + */ + setValue(value: number): boolean { + this.target = this.wrap ? wrap(value, this.max) : MathUtils.clamp(value, this.min, this.max); + this.mode = DynamicMode.STOP; + this.currentSpeed = 0; + if (this.target !== this.current) { + this.current = this.target; + if (this.fn) { + this.fn(this.current); + } + return true; + } + return false; + } + + /** + * @internal + */ + update(elapsed: number): boolean { + // in position mode switch to stop mode when in the decceleration window + if (this.mode === DynamicMode.POSITION) { + // in loop mode, alter "current" to avoid crossing the origin + if (this.wrap && Math.abs(this.target - this.current) > this.max / 2) { + this.current = this.current < this.target ? this.current + this.max : this.current - this.max; + } + + const dstStop = (this.currentSpeed * this.currentSpeed) / (this.speed * this.speedMult * 4); + if (Math.abs(this.target - this.current) <= dstStop) { + this.mode = DynamicMode.STOP; + } + } + + // compute speed + let targetSpeed = this.mode === DynamicMode.STOP ? 0 : this.speed * this.speedMult; + if (this.target < this.current) { + targetSpeed = -targetSpeed; + } + if (this.currentSpeed < targetSpeed) { + this.currentSpeed = Math.min( + targetSpeed, + this.currentSpeed + (elapsed / 1000) * this.speed * this.speedMult * 2 + ); + } else if (this.currentSpeed > targetSpeed) { + this.currentSpeed = Math.max( + targetSpeed, + this.currentSpeed - (elapsed / 1000) * this.speed * this.speedMult * 2 + ); + } + + // compute new position + let next = null; + if (this.current > this.target && this.currentSpeed) { + next = Math.max(this.target, this.current + (this.currentSpeed * elapsed) / 1000); + } else if (this.current < this.target && this.currentSpeed) { + next = Math.min(this.target, this.current + (this.currentSpeed * elapsed) / 1000); + } + + // apply value + if (next !== null) { + next = this.wrap ? wrap(next, this.max) : MathUtils.clamp(next, this.min, this.max); + if (next !== this.current) { + this.current = next; + if (this.fn) { + this.fn(this.current); + } + return true; + } + } + + return false; + } +} diff --git a/packages/core/src/utils/MultiDynamic.ts b/packages/core/src/utils/MultiDynamic.ts new file mode 100644 index 000000000..3b7ff82dc --- /dev/null +++ b/packages/core/src/utils/MultiDynamic.ts @@ -0,0 +1,99 @@ +import { Dynamic } from './Dynamic'; + +/** + * Wrapper for multiple {@link Dynamic} evolving together + */ +export class MultiDynamic> { + get current(): Record { + return Object.entries(this.dynamics).reduce((values, [name, dynamic]) => { + // @ts-ignore + values[name] = dynamic.current; + return values; + }, {} as Record); + } + + constructor(private readonly fn: (val: Record) => void, private readonly dynamics: T) { + if (this.fn) { + this.fn(this.current); + } + } + + /** + * Changes base speed + */ + setSpeed(speed: number) { + for (const d of Object.values(this.dynamics)) { + d.setSpeed(speed); + } + } + + /** + * Defines the target positions + */ + goto(positions: Partial>, speedMult = 1) { + for (const [name, position] of Object.entries(positions)) { + this.dynamics[name].goto(position as number, speedMult); + } + } + + /** + * Increase/decrease the target positions + */ + step(steps: Partial>, speedMult = 1) { + for (const [name, step] of Object.entries(steps)) { + this.dynamics[name].step(step as number, speedMult); + } + } + + /** + * Starts infinite movements + */ + roll(rolls: Partial>, speedMult = 1) { + for (const [name, roll] of Object.entries(rolls)) { + this.dynamics[name].roll(roll, speedMult); + } + } + + /** + * Stops movements + */ + stop() { + for (const d of Object.values(this.dynamics)) { + d.stop(); + } + } + + /** + * Defines the current positions and immediately stops movements + */ + setValue(values: Partial>): boolean { + let hasUpdates = false; + + for (const [name, value] of Object.entries(values)) { + hasUpdates = this.dynamics[name].setValue(value as number) || hasUpdates; + } + + if (hasUpdates && this.fn) { + this.fn(this.current); + } + + return hasUpdates; + } + + /** + * @internal + */ + update(elapsed: number): boolean { + let hasUpdates = false; + + for (const d of Object.values(this.dynamics)) { + hasUpdates = d.update(elapsed) || hasUpdates; + } + + if (hasUpdates && this.fn) { + this.fn(this.current); + } + + return hasUpdates; + } +} diff --git a/packages/core/src/utils/PressHandler.spec.ts b/packages/core/src/utils/PressHandler.spec.ts new file mode 100644 index 000000000..a1f69aa3e --- /dev/null +++ b/packages/core/src/utils/PressHandler.spec.ts @@ -0,0 +1,32 @@ +import assert from 'assert'; +import { PressHandler } from './PressHandler'; + +describe('utils:PressHandler', () => { + it('should wait at least X ms before exec', (done) => { + const handler = new PressHandler(100); + + const start = new Date().getTime(); + + handler.down(); + handler.up(() => { + const elapsed = new Date().getTime() - start; + assert.ok(elapsed >= 100, `Expected ${elapsed} to be greater than 100`); + done(); + }); + }); + + it('should exec immediately if X ms already elapsed', (done) => { + const handler = new PressHandler(100); + + handler.down(); + + setTimeout(() => { + const start = new Date().getTime(); + handler.up(() => { + const elapsed = new Date().getTime() - start; + assert.ok(elapsed < 20, `Expected ${elapsed} to be lower than 20`); + done(); + }); + }, 200); + }); +}); diff --git a/packages/core/src/utils/PressHandler.ts b/packages/core/src/utils/PressHandler.ts new file mode 100644 index 000000000..59c3530f1 --- /dev/null +++ b/packages/core/src/utils/PressHandler.ts @@ -0,0 +1,44 @@ +/** + * Helper for pressable things (buttons, keyboard) + * @description When the pressed thing goes up and was not pressed long enough, wait a bit more before execution + * @internal + */ +export class PressHandler { + private time = 0; + private timeout: ReturnType; + + get pending() { + return this.time !== 0; + } + + constructor(private readonly delay = 200) { + this.delay = delay; + } + + down() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } + + this.time = new Date().getTime(); + } + + up(cb: () => void) { + if (!this.time) { + return; + } + + const elapsed = Date.now() - this.time; + if (elapsed < this.delay) { + this.timeout = setTimeout(() => { + cb(); + this.timeout = undefined; + this.time = 0; + }, this.delay); + } else { + cb(); + this.time = 0; + } + } +} diff --git a/packages/core/src/utils/Slider.ts b/packages/core/src/utils/Slider.ts new file mode 100644 index 000000000..3568842f2 --- /dev/null +++ b/packages/core/src/utils/Slider.ts @@ -0,0 +1,166 @@ +/** + * Direction of a {@link Slider} + */ +export enum SliderDirection { + VERTICAL = 'VERTICAL', + HORIZONTAL = 'HORIZONTAL', +} + +/** + * Data transmitted to the {@link Slider} listener + */ +export type SliderUpdateData = { + /** + * slider progression for 0-1 + */ + readonly value: number; + + /** + * the user clicked on the slider + */ + readonly click: boolean; + + /** + * the user moves the cursor above the slider, without click + */ + readonly mouseover: boolean; + + /** + * the user moves the cursor above the slider while maintaining click + */ + readonly mousedown: boolean; + + /** + * the cursor position on the page + */ + readonly cursor: { clientX: number; clientY: number }; +}; + +/** + * Helper to make sliders elements + */ +export class Slider { + private mousedown = false; + private mouseover = false; + + get isVertical() { + return this.direction === SliderDirection.VERTICAL; + } + + get isHorizontal() { + return this.direction === SliderDirection.HORIZONTAL; + } + + constructor( + /** main container of the sliding element */ + private readonly container: HTMLElement, + /** direction of the slider */ + private readonly direction: SliderDirection, + /** callback when the user interacts with the slider */ + private readonly listener: (data: SliderUpdateData) => void + ) { + this.container.addEventListener('click', this); + this.container.addEventListener('mousedown', this); + this.container.addEventListener('mouseenter', this); + this.container.addEventListener('mouseleave', this); + this.container.addEventListener('touchstart', this); + this.container.addEventListener('mousemove', this, true); + this.container.addEventListener('touchmove', this, true); + window.addEventListener('mouseup', this); + window.addEventListener('touchend', this); + } + + destroy() { + window.removeEventListener('mouseup', this); + window.removeEventListener('touchend', this); + } + + /** + * @internal + */ + handleEvent(e: Event) { + // prettier-ignore + switch (e.type) { + case 'click': e.stopPropagation(); break; + case 'mousedown': this.__onMouseDown(e as MouseEvent); break; + case 'mouseenter': this.__onMouseEnter(e as MouseEvent); break; + case 'mouseleave': this.__onMouseLeave(e as MouseEvent); break; + case 'touchstart': this.__onTouchStart(e as TouchEvent); break; + case 'mousemove': this.__onMouseMove(e as MouseEvent); break; + case 'touchmove': this.__onTouchMove(e as TouchEvent); break; + case 'mouseup': this.__onMouseUp(e as MouseEvent); break; + case 'touchend': this.__onTouchEnd(e as TouchEvent); break; + } + } + + private __onMouseDown(evt: MouseEvent) { + this.mousedown = true; + this.__update(evt.clientX, evt.clientY, true); + } + + private __onMouseEnter(evt: MouseEvent) { + this.mouseover = true; + this.__update(evt.clientX, evt.clientY, true); + } + + private __onTouchStart(evt: TouchEvent) { + this.mouseover = true; + this.mousedown = true; + const touch = evt.changedTouches[0]; + this.__update(touch.clientX, touch.clientY, true); + } + + private __onMouseMove(evt: MouseEvent) { + if (this.mousedown || this.mouseover) { + evt.stopPropagation(); + this.__update(evt.clientX, evt.clientY, true); + } + } + + private __onTouchMove(evt: TouchEvent) { + if (this.mousedown || this.mouseover) { + evt.stopPropagation(); + const touch = evt.changedTouches[0]; + this.__update(touch.clientX, touch.clientY, true); + } + } + + private __onMouseUp(evt: MouseEvent) { + if (this.mousedown) { + this.mousedown = false; + this.__update(evt.clientX, evt.clientY, false); + } + } + + private __onMouseLeave(evt: MouseEvent) { + if (this.mouseover) { + this.mouseover = false; + this.__update(evt.clientX, evt.clientY, true); + } + } + + private __onTouchEnd(evt: TouchEvent) { + if (this.mousedown) { + this.mouseover = false; + this.mousedown = false; + const touch = evt.changedTouches[0]; + this.__update(touch.clientX, touch.clientY, false); + } + } + + private __update(clientX: number, clientY: number, moving: boolean) { + const boundingClientRect = this.container.getBoundingClientRect(); + const cursor = this.isVertical ? clientY : clientX; + const pos = boundingClientRect[this.isVertical ? 'bottom' : 'left']; + const size = boundingClientRect[this.isVertical ? 'height' : 'width']; + const val = Math.abs((pos - cursor) / size); + + this.listener({ + value: val, + click: !moving, + mousedown: this.mousedown, + mouseover: this.mouseover, + cursor: { clientX, clientY }, + }); + } +} diff --git a/packages/core/src/utils/browser.ts b/packages/core/src/utils/browser.ts new file mode 100644 index 000000000..f9fd04dfc --- /dev/null +++ b/packages/core/src/utils/browser.ts @@ -0,0 +1,101 @@ +import { Point } from '../model'; + +/** + * Get an element in the page by an unknown selector + */ +export function getElement(selector: string | HTMLElement): HTMLElement { + if (typeof selector === 'string') { + return selector.match(/^[a-z]/i) ? document.getElementById(selector) : document.querySelector(selector); + } else { + return selector; + } +} + +/** + * Toggles a CSS class + */ +export function toggleClass(element: Element, className: string, active?: boolean) { + if (active === undefined) { + element.classList.toggle(className); + } else if (active) { + element.classList.add(className); + } else if (!active) { + element.classList.remove(className); + } +} + +/** + * Adds one or several CSS classes to an element + */ +export function addClasses(element: Element, className: string) { + element.classList.add(...className.split(' ')); +} + +/** + * Removes one or several CSS classes to an element + */ +export function removeClasses(element: Element, className: string) { + element.classList.remove(...className.split(' ')); +} + +/** + * Searches if an element has a particular parent at any level including itself + */ +export function hasParent(el: HTMLElement, parent: Element): boolean { + let test: HTMLElement | null = el; + + do { + if (test === parent) { + return true; + } + test = test.parentElement; + } while (test); + + return false; +} + +/** + * Gets the closest parent (can by itself) + */ +export function getClosest(el: HTMLElement, selector: string): HTMLElement | null { + // When el is document or window, the matches does not exist + if (!el?.matches) { + return null; + } + + let test: HTMLElement | null = el; + + do { + if (test.matches(selector)) { + return test; + } + test = test.parentElement; + } while (test); + + return null; +} + +/** + * Gets the position of an element in the viewer without reflow + * @description Will gives the same result as getBoundingClientRect() as soon as there are no CSS transforms + */ +export function getPosition(el: HTMLElement): Point { + let x = 0; + let y = 0; + let test: HTMLElement | null = el; + + while (test) { + x += test.offsetLeft - test.scrollLeft + test.clientLeft; + y += test.offsetTop - test.scrollTop + test.clientTop; + test = test.offsetParent as HTMLElement; + } + + return { x, y }; +} + +/** + * Gets an element style value + */ +export function getStyle(elt: Element, prop: string): string { + return (window.getComputedStyle(elt, null) as any)[prop]; +} diff --git a/src/utils/index.js b/packages/core/src/utils/index.ts similarity index 86% rename from src/utils/index.js rename to packages/core/src/utils/index.ts index 09323ac48..6441020b1 100644 --- a/src/utils/index.js +++ b/packages/core/src/utils/index.ts @@ -1,7 +1,3 @@ -/** - * @namespace PSV.utils - */ - export * from './browser'; export * from './math'; export * from './misc'; diff --git a/packages/core/src/utils/math.ts b/packages/core/src/utils/math.ts new file mode 100644 index 000000000..dffd0fd06 --- /dev/null +++ b/packages/core/src/utils/math.ts @@ -0,0 +1,68 @@ +import { Point, Position } from '../model'; + +/** + * Ensures a value is within 0 and `max` by wrapping max to 0 + */ +export function wrap(value: number, max: number): number { + let result = value % max; + + if (result < 0) { + result += max; + } + + return result; +} + +/** + * Computes the sum of an array + */ +export function sum(array: number[]): number { + return array.reduce((a, b) => a + b, 0); +} + +/** + * Computes the distance between two points + */ +export function distance(p1: Point, p2: Point): number { + return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); +} + +/** + * Compute the shortest offset between two angles on a sphere + */ +export function getShortestArc(from: number, to: number): number { + const candidates = [ + 0, // direct + Math.PI * 2, // clock-wise cross zero + -Math.PI * 2, // counter-clock-wise cross zero + ]; + + return candidates.reduce((value, candidate) => { + const newCandidate = to - from + candidate; + return Math.abs(newCandidate) < Math.abs(value) ? newCandidate : value; + }, Infinity); +} + +/** + * Computes the angle between the current position and a target position + */ +export function getAngle(position1: Position, position2: Position): number { + // prettier-ignore + return Math.acos( + Math.cos(position1.yaw) + * Math.cos(position2.pitch) + * Math.cos(position1.yaw - position2.yaw) + + Math.sin(position1.pitch) + * Math.sin(position2.pitch) + ); +} + +/** + * Returns the distance between two points on a sphere of radius one + * @link http://www.movable-type.co.uk/scripts/latlong.html + */ +export function greatArcDistance([lon1, lat1]: [number, number], [lon2, lat2]: [number, number]): number { + const x = (lon2 - lon1) * Math.cos((lat1 + lat2) / 2); + const y = lat2 - lat1; + return Math.sqrt(x * x + y * y); +} diff --git a/packages/core/src/utils/misc.spec.ts b/packages/core/src/utils/misc.spec.ts new file mode 100644 index 000000000..e5198868c --- /dev/null +++ b/packages/core/src/utils/misc.spec.ts @@ -0,0 +1,103 @@ +import assert from 'assert'; +import { dasherize, deepEqual, deepmerge } from './misc'; + +describe('utils:misc:deepmerge', () => { + it('should merge basic plain objects', () => { + const one = { a: 'z', b: { c: { d: 'e' } } }; + const two = { b: { c: { f: 'g', j: 'i' } } }; + + const result = deepmerge(one, two); + + assert.deepStrictEqual(one, { a: 'z', b: { c: { d: 'e', f: 'g', j: 'i' } } }); + assert.strictEqual(result, one); + }); + + it('should merge arrays by replace', () => { + const one = { a: [1, 2, 3] }; + const two = { a: [2, 4] }; + + const result = deepmerge(one, two); + + assert.deepStrictEqual(one, { a: [2, 4] }); + assert.strictEqual(result, one); + }); + + it('should clone object', () => { + const one = { b: { c: { d: 'e' } } }; + + const result = deepmerge(null, one); + + assert.deepStrictEqual(result, { b: { c: { d: 'e' } } }); + assert.notStrictEqual(result, one); + assert.notStrictEqual(result.b.c, one.b.c); + }); + + it('should clone array', () => { + const one = [{ a: 'b' }, { c: 'd' }]; + + const result = deepmerge(null, one); + + assert.deepStrictEqual(result, [{ a: 'b' }, { c: 'd' }]); + assert.notStrictEqual(result[0], one[1]); + }); + + it('should accept primitives', () => { + const one = 'foo'; + const two = 'bar'; + + const result = deepmerge(one, two); + + assert.strictEqual(result, 'bar'); + }); + + it('should stop on recursion', () => { + const one: any = { a: 'foo' }; + one.b = one; + + const result = deepmerge(null, one); + + assert.deepStrictEqual(result, { a: 'foo' }); + }); +}); + +describe('utils:misc:dasherize', () => { + it('should dasherize from camelCase', () => { + assert.strictEqual(dasherize('strokeWidth'), 'stroke-width'); + }); + + it('should not change existing dash-case', () => { + assert.strictEqual(dasherize('stroke-width'), 'stroke-width'); + }); +}); + +describe('utils:misc:deepEqual', () => { + it('should compare simple objects', () => { + assert.strictEqual(deepEqual({ foo: 'bar' }, { foo: 'bar' }), true); + + assert.strictEqual(deepEqual({ foo: 'bar' }, { foo: 'foo' }), false); + + assert.strictEqual(deepEqual({ foo: 'bar' }, { foo: 'bar', baz: 'bar' }), false); + }); + + it('should compare nested objects', () => { + assert.strictEqual(deepEqual({ foo: { bar: 'baz' } }, { foo: { bar: 'baz' } }), true); + + assert.strictEqual(deepEqual({ foo: { bar: 'baz' } }, { foo: { bar: 'foo' } }), false); + + assert.strictEqual(deepEqual({ foo: { bar: 'baz' } }, { foo: { bar: 'baz', baz: 'bar' } }), false); + }); + + it('should compare arrays', () => { + assert.strictEqual(deepEqual({ foo: ['bar', 'baz'] }, { foo: ['bar', 'baz'] }), true); + + assert.strictEqual(deepEqual({ foo: ['bar', 'baz'] }, { foo: ['bar', 'bar'] }), false); + }); + + it('should compare standard types', () => { + assert.strictEqual(deepEqual({ a: 'foo', b: false, c: -4 }, { a: 'foo', b: false, c: -4 }), true); + + assert.strictEqual(deepEqual({ a: 'foo', b: false, c: -4 }, { a: 'foo', b: 'false', c: -4 }), false); + + assert.strictEqual(deepEqual({ a: 'foo', b: false, c: -4 }, { a: 'foo', b: false, c: '-4' }), false); + }); +}); diff --git a/packages/core/src/utils/misc.ts b/packages/core/src/utils/misc.ts new file mode 100644 index 000000000..697ab4f5d --- /dev/null +++ b/packages/core/src/utils/misc.ts @@ -0,0 +1,147 @@ +/** + * Transforms a string to dash-case + * @link https://github.com/shahata/dasherize + */ +export function dasherize(str: string): string { + return str.replace(/[A-Z](?:(?=[^A-Z])|[A-Z]*(?=[A-Z][^A-Z]|$))/g, (s, i) => { + return (i > 0 ? '-' : '') + s.toLowerCase(); + }); +} + +/** + * Returns a function, that, when invoked, will only be triggered at most once during a given window of time. + */ +export function throttle any>(callback: T, wait: number): (...args: Parameters) => void { + let paused = false; + return function (this: any, ...args: Parameters) { + if (!paused) { + paused = true; + setTimeout(() => { + callback.apply(this, args); + paused = false; + }, wait); + } + }; +} + +/** + * Test if an object is a plain object + * @description Test if an object is a plain object, i.e. is constructed + * by the built-in Object constructor and inherits directly from Object.prototype + * or null. + * @link https://github.com/lodash/lodash/blob/master/isPlainObject.js + */ +export function isPlainObject>(value: any): value is T { + if (typeof value !== 'object' || value === null || Object.prototype.toString.call(value) !== '[object Object]') { + return false; + } + if (Object.getPrototypeOf(value) === null) { + return true; + } + let proto = value; + while (Object.getPrototypeOf(proto) !== null) { + proto = Object.getPrototypeOf(proto); + } + return Object.getPrototypeOf(value) === proto; +} + +/** + * Merges the enumerable attributes of two objects + * @description Replaces arrays and alters the target object. + * @copyright Nicholas Fisher + */ +export function deepmerge(target: T, src: T): T { + const first = src; + + return (function merge(target: any, src: any) { + if (Array.isArray(src)) { + if (!target || !Array.isArray(target)) { + target = []; + } else { + target.length = 0; + } + src.forEach((e, i) => { + target[i] = merge(null, e); + }); + } else if (typeof src === 'object') { + if (!target || Array.isArray(target)) { + target = {}; + } + Object.keys(src).forEach((key) => { + if (typeof src[key] !== 'object' || !src[key] || !isPlainObject(src[key])) { + target[key] = src[key]; + } else if (src[key] != first) { + if (!target[key]) { + target[key] = merge(null, src[key]); + } else { + merge(target[key], src[key]); + } + } + }); + } else { + target = src; + } + + return target; + })(target, src); +} + +/** + * Deeply clones an object + */ +export function clone(src: T): T { + return deepmerge(null as T, src); +} + +/** + * Tests of an object is empty + */ +export function isEmpty(obj: any): boolean { + return !obj || (Object.keys(obj).length === 0 && obj.constructor === Object); +} + +/** + * Returns if a valu is null or undefined + */ +export function isNil(val: any): val is null | undefined { + return val === null || val === undefined; +} + +/** + * Returns the first non null non undefined parameter + */ +export function firstNonNull(...values: T[]): T | null { + for (const val of values) { + if (!isNil(val)) { + return val; + } + } + + return null; +} + +/** + * Returns deep equality between objects + * @link https://gist.github.com/egardner/efd34f270cc33db67c0246e837689cb9 + */ +export function deepEqual(obj1: any, obj2: any): boolean { + if (obj1 === obj2) { + return true; + } else if (isObject(obj1) && isObject(obj2)) { + if (Object.keys(obj1).length !== Object.keys(obj2).length) { + return false; + } + for (const prop of Object.keys(obj1)) { + if (!deepEqual(obj1[prop], obj2[prop])) { + return false; + } + } + return true; + } else { + return false; + } +} + +function isObject(obj: any): boolean { + return typeof obj === 'object' && obj != null; +} diff --git a/packages/core/src/utils/psv.spec.ts b/packages/core/src/utils/psv.spec.ts new file mode 100644 index 000000000..8f5c4c3bd --- /dev/null +++ b/packages/core/src/utils/psv.spec.ts @@ -0,0 +1,368 @@ +import assert from 'assert'; +import { cleanCssPosition, getXMPValue, parseAngle, parsePoint, parseSpeed } from './psv'; + +describe('utils:psv:parseAngle', () => { + it('should normalize number', () => { + assert.strictEqual(parseAngle(0), 0, '0'); + assert.strictEqual(parseAngle(Math.PI), Math.PI, 'PI'); + assert.strictEqual(parseAngle(3 * Math.PI), Math.PI, '3xPI'); + + assert.strictEqual(parseAngle(0, true), 0, '0 centered'); + assert.strictEqual(parseAngle((Math.PI * 3) / 4, true), Math.PI / 2, '3/4xPI centered'); + assert.strictEqual(parseAngle((-Math.PI * 3) / 4, true), -Math.PI / 2, '-3/4xPI centered'); + }); + + it('should parse radians angles', () => { + const values: Record = { + '0': 0, + '1.72': 1.72, + '-2.56': Math.PI * 2 - 2.56, + '3.14rad': 3.14, + '-3.14rad': Math.PI * 2 - 3.14, + }; + + for (const pos in values) { + assert.strictEqual(parseAngle(pos).toFixed(16), values[pos].toFixed(16), pos); + } + }); + + it('should parse degrees angles', () => { + const values: Record = { + '0deg': 0, + '30deg': (30 * Math.PI) / 180, + '-30deg': Math.PI * 2 - (30 * Math.PI) / 180, + '85degs': (85 * Math.PI) / 180, + '360degs': 0, + }; + + for (const pos in values) { + assert.strictEqual(parseAngle(pos).toFixed(16), values[pos].toFixed(16), pos); + } + }); + + it('should normalize angles between 0 and 2Pi', () => { + const values: Record = { + '450deg': Math.PI / 2, + '1440deg': 0, + '8.15': 8.15 - Math.PI * 2, + '-3.14': Math.PI * 2 - 3.14, + '-360deg': 0, + }; + + for (const pos in values) { + assert.strictEqual(parseAngle(pos).toFixed(16), values[pos].toFixed(16), pos); + } + }); + + it('should normalize angles between -Pi/2 and Pi/2', () => { + const values: Record = { + '45deg': Math.PI / 4, + '-4': Math.PI / 2, + }; + + for (const pos in values) { + assert.strictEqual(parseAngle(pos, true).toFixed(16), values[pos].toFixed(16), pos); + } + }); + + it('should normalize angles between -Pi and Pi', function () { + const values: Record = { + '45deg': Math.PI / 4, + '4': -2 * Math.PI + 4, + }; + + for (const pos in values) { + assert.strictEqual(parseAngle(pos, true, false).toFixed(16), values[pos].toFixed(16), pos); + } + }); + + it('should throw exception on invalid values', () => { + assert.throws( + () => { + parseAngle('foobar'); + }, + /Unknown angle "foobar"/, + 'foobar' + ); + + assert.throws( + () => { + parseAngle('200gr'); + }, + /Unknown angle unit "gr"/, + '200gr' + ); + }); +}); + +describe('utils:psv:parsePosition', () => { + it('should parse 2 keywords', () => { + const values: Record = { + 'top left': { x: 0, y: 0 }, + 'top center': { x: 0.5, y: 0 }, + 'top right': { x: 1, y: 0 }, + 'center left': { x: 0, y: 0.5 }, + 'center center': { x: 0.5, y: 0.5 }, + 'center right': { x: 1, y: 0.5 }, + 'bottom left': { x: 0, y: 1 }, + 'bottom center': { x: 0.5, y: 1 }, + 'bottom right': { x: 1, y: 1 }, + }; + + for (const pos in values) { + assert.deepStrictEqual(parsePoint(pos), values[pos], pos); + + const rev = pos.split(' ').reverse().join(' '); + assert.deepStrictEqual(parsePoint(rev), values[pos], rev); + } + }); + + it('should parse 1 keyword', () => { + const values: Record = { + top: { x: 0.5, y: 0 }, + center: { x: 0.5, y: 0.5 }, + bottom: { x: 0.5, y: 1 }, + left: { x: 0, y: 0.5 }, + right: { x: 1, y: 0.5 }, + }; + + for (const pos in values) { + assert.deepStrictEqual(parsePoint(pos), values[pos], pos); + } + }); + + it('should parse 2 percentages', () => { + const values: Record = { + '0% 0%': { x: 0, y: 0 }, + '50% 50%': { x: 0.5, y: 0.5 }, + '100% 100%': { x: 1, y: 1 }, + '10% 80%': { x: 0.1, y: 0.8 }, + '80% 10%': { x: 0.8, y: 0.1 }, + }; + + for (const pos in values) { + assert.deepStrictEqual(parsePoint(pos), values[pos], pos); + } + }); + + it('should parse 1 percentage', () => { + const values: Record = { + '0%': { x: 0, y: 0 }, + '50%': { x: 0.5, y: 0.5 }, + '100%': { x: 1, y: 1 }, + '80%': { x: 0.8, y: 0.8 }, + }; + + for (const pos in values) { + assert.deepStrictEqual(parsePoint(pos), values[pos], pos); + } + }); + + it('should parse mixed keyword & percentage', () => { + const values: Record = { + 'top 80%': { x: 0.8, y: 0 }, + '80% bottom': { x: 0.8, y: 1 }, + 'left 40%': { x: 0, y: 0.4 }, + '40% right': { x: 1, y: 0.4 }, + 'center 10%': { x: 0.5, y: 0.1 }, + '10% center': { x: 0.1, y: 0.5 }, + }; + + for (const pos in values) { + assert.deepStrictEqual(parsePoint(pos), values[pos], pos); + } + }); + + it('should fallback on parse fail', () => { + const values: Record = { + '': { x: 0.5, y: 0.5 }, + 'crap': { x: 0.5, y: 0.5 }, + 'foo bar': { x: 0.5, y: 0.5 }, + 'foo 50%': { x: 0.5, y: 0.5 }, + '%': { x: 0.5, y: 0.5 }, + }; + + for (const pos in values) { + assert.deepStrictEqual(parsePoint(pos), values[pos], pos); + } + }); + + it('should ignore extra tokens', () => { + const values: Record = { + 'top center bottom': { x: 0.5, y: 0 }, + '50% left 20%': { x: 0, y: 0.5 }, + '0% 0% okay this time it goes ridiculous': { x: 0, y: 0 }, + }; + + for (const pos in values) { + assert.deepStrictEqual(parsePoint(pos), values[pos], pos); + } + }); + + it('should ignore case', () => { + const values: Record = { + 'TOP CENTER': { x: 0.5, y: 0 }, + 'cenTer LefT': { x: 0, y: 0.5 }, + }; + + for (const pos in values) { + assert.deepStrictEqual(parsePoint(pos), values[pos], pos); + } + }); +}); + +describe('utils:psv:parseSpeed', () => { + it('should parse all units', () => { + const values: Record = { + '360dpm': (360 * Math.PI) / 180 / 60, + '360degrees per minute': (360 * Math.PI) / 180 / 60, + '10dps': (10 * Math.PI) / 180, + '10degrees per second': (10 * Math.PI) / 180, + '2radians per minute': 2 / 60, + '0.1radians per second': 0.1, + '2rpm': (2 * 2 * Math.PI) / 60, + '2revolutions per minute': (2 * 2 * Math.PI) / 60, + '0.01rps': 0.01 * 2 * Math.PI, + '0.01revolutions per second': 0.01 * 2 * Math.PI, + }; + + for (const speed in values) { + assert.strictEqual(parseSpeed(speed).toFixed(16), values[speed].toFixed(16), speed); + } + }); + + it('should allow various forms', () => { + const values: Record = { + '2rpm': (2 * 2 * Math.PI) / 60, + '2 rpm': (2 * 2 * Math.PI) / 60, + '2revolutions per minute': (2 * 2 * Math.PI) / 60, + '2 revolutions per minute': (2 * 2 * Math.PI) / 60, + '-2rpm': (-2 * 2 * Math.PI) / 60, + '-2 rpm': (-2 * 2 * Math.PI) / 60, + '-2revolutions per minute': (-2 * 2 * Math.PI) / 60, + '-2 revolutions per minute': (-2 * 2 * Math.PI) / 60, + }; + + for (const speed in values) { + assert.strictEqual(parseSpeed(speed).toFixed(16), values[speed].toFixed(16), speed); + } + }); + + it('should throw exception on invalid unit', () => { + assert.throws( + () => { + parseSpeed('10rpsec'); + }, + /Unknown speed unit "rpsec"/, + '10rpsec' + ); + }); + + it('should passthrough when number', () => { + assert.strictEqual(parseSpeed(Math.PI), Math.PI); + }); +}); + +describe('utils:psv:getXMPValue', () => { + it('should parse XMP data with children', () => { + const data = + '\ + equirectangular\ + True\ + 5376\ + 2688\ + 5376\ + 2688\ + 0\ + 0\ + 270.0\ + 0\ + 0.2\ + '; + + assert.deepStrictEqual( + [ + getXMPValue(data, 'FullPanoWidthPixels'), + getXMPValue(data, 'FullPanoHeightPixels'), + getXMPValue(data, 'CroppedAreaImageWidthPixels'), + getXMPValue(data, 'CroppedAreaImageHeightPixels'), + getXMPValue(data, 'CroppedAreaLeftPixels'), + getXMPValue(data, 'CroppedAreaTopPixels'), + getXMPValue(data, 'PoseHeadingDegrees'), + getXMPValue(data, 'PosePitchDegrees'), + getXMPValue(data, 'PoseRollDegrees'), + ], + [5376, 2688, 5376, 2688, 0, 0, 270, 0, 0] + ); + }); + + it('should parse XMP data with attributes', () => { + const data = + ''; + + assert.deepStrictEqual( + [ + getXMPValue(data, 'FullPanoWidthPixels'), + getXMPValue(data, 'FullPanoHeightPixels'), + getXMPValue(data, 'CroppedAreaImageWidthPixels'), + getXMPValue(data, 'CroppedAreaImageHeightPixels'), + getXMPValue(data, 'CroppedAreaLeftPixels'), + getXMPValue(data, 'CroppedAreaTopPixels'), + getXMPValue(data, 'PoseHeadingDegrees'), + getXMPValue(data, 'PosePitchDegrees'), + getXMPValue(data, 'PoseRollDegrees'), + ], + [5376, 2688, 5376, 2688, 0, 0, 270, 0, 0] + ); + }); +}); + +describe('utils:psv:cleanPosition', () => { + it('should clean various formats', () => { + assert.deepStrictEqual(cleanCssPosition('top right'), ['top', 'right']); + assert.deepStrictEqual(cleanCssPosition('right top'), ['top', 'right']); + assert.deepStrictEqual(cleanCssPosition(['top', 'right']), ['top', 'right']); + }); + + it('should add missing center', () => { + assert.deepStrictEqual(cleanCssPosition('top'), ['top', 'center']); + assert.deepStrictEqual(cleanCssPosition('left'), ['center', 'left']); + assert.deepStrictEqual(cleanCssPosition('center'), ['center', 'center']); + }); + + it('should dissallow all center', () => { + assert.strictEqual(cleanCssPosition('center center', { allowCenter: false, cssOrder: true }), null); + assert.strictEqual(cleanCssPosition('center', { allowCenter: false, cssOrder: true }), null); + }); + + it('should return null on unparsable values', () => { + assert.strictEqual(cleanCssPosition('foo bar'), null); + assert.strictEqual(cleanCssPosition('TOP CENTER'), null); + assert.strictEqual(cleanCssPosition(''), null); + assert.strictEqual(cleanCssPosition(undefined), null); + }); + + it('should allow XY order', () => { + assert.deepStrictEqual(cleanCssPosition('right top', { allowCenter: true, cssOrder: false }), ['right', 'top']); + assert.deepStrictEqual(cleanCssPosition(['top', 'right'], { allowCenter: true, cssOrder: false }), [ + 'top', + 'right', + ]); + }); + + it('should always order with center', () => { + assert.deepStrictEqual(cleanCssPosition('center top'), ['top', 'center']); + assert.deepStrictEqual(cleanCssPosition('left center'), ['center', 'left']); + }); +}); diff --git a/packages/core/src/utils/psv.ts b/packages/core/src/utils/psv.ts new file mode 100644 index 000000000..c45d59dd0 --- /dev/null +++ b/packages/core/src/utils/psv.ts @@ -0,0 +1,402 @@ +import { Euler, LinearFilter, MathUtils, Quaternion, Texture, Vector3 } from 'three'; +import { ExtendedPosition, Point, Position, PositionCompat } from '../model'; +import { PSVError } from '../PSVError'; +import { wrap } from './math'; +import { clone } from './misc'; + +/** + * @deprecated + */ +export function positionCompat(position: T): PositionCompat & T { + return { + ...position, + get longitude() { + logWarn('longitude is deprecated, use yaw instead'); + return this.yaw; + }, + get latitude() { + logWarn('latitude is deprecated, use pitch instead'); + return this.pitch; + }, + }; +} + +/** + * Builds an Error with name 'AbortError' + */ +export function getAbortError(): Error { + const error = new Error('Loading was aborted.'); + error.name = 'AbortError'; + return error; +} + +/** + * Tests if an Error has name 'AbortError' + */ +export function isAbortError(err: Error): boolean { + return err?.name === 'AbortError'; +} + +/** + * Displays a warning in the console with "PhotoSphereViewer" prefix + */ +export function logWarn(message: string) { + console.warn(`PhotoSphereViewer: ${message}`); +} + +/** + * Checks if an object is a ExtendedPosition, ie has textureX/textureY or yaw/pitch + */ +export function isExtendedPosition(object: any): object is ExtendedPosition { + if (!object) { + return false; + } + return [ + ['textureX', 'textureY'], + ['yaw', 'pitch'], + ['x', 'y'], + ['longitude', 'latitude'], + ].some(([key1, key2]) => { + return object[key1] !== undefined && object[key2] !== undefined; + }); +} + +/** + * Returns the value of a given attribute in the panorama metadata + */ +export function getXMPValue(data: string, attr: string): number | null { + // XMP data are stored in children + let result = data.match('(.*)'); + if (result !== null) { + const val = parseInt(result[1], 10); + return isNaN(val) ? null : val; + } + + // XMP data are stored in attributes + result = data.match('GPano:' + attr + '="(.*?)"'); + if (result !== null) { + const val = parseInt(result[1], 10); + return isNaN(val) ? null : val; + } + + return null; +} + +const CSS_POSITIONS: Record = { + top: '0%', + bottom: '100%', + left: '0%', + right: '100%', + center: '50%', +}; +const X_VALUES = ['left', 'center', 'right']; +const Y_VALUES = ['top', 'center', 'bottom']; +const POS_VALUES = [...X_VALUES, ...Y_VALUES]; +const CENTER = 'center'; + +/** + * Translate CSS values like "top center" or "10% 50%" as top and left positions (0-1 range) + * @description The implementation is as close as possible to the "background-position" specification + * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-position} + */ +export function parsePoint(value: string | Point): Point { + if (!value) { + return { x: 0.5, y: 0.5 }; + } + + if (typeof value === 'object') { + return value; + } + + let tokens = value.toLocaleLowerCase().split(' ').slice(0, 2); + + if (tokens.length === 1) { + if (CSS_POSITIONS[tokens[0]]) { + tokens = [tokens[0], CENTER]; + } else { + tokens = [tokens[0], tokens[0]]; + } + } + + const xFirst = tokens[1] !== 'left' && tokens[1] !== 'right' && tokens[0] !== 'top' && tokens[0] !== 'bottom'; + + tokens = tokens.map((token) => CSS_POSITIONS[token] || token); + + if (!xFirst) { + tokens.reverse(); + } + + const parsed = tokens.join(' ').match(/^([0-9.]+)% ([0-9.]+)%$/); + + if (parsed) { + return { + x: parseFloat(parsed[1]) / 100, + y: parseFloat(parsed[2]) / 100, + }; + } else { + return { x: 0.5, y: 0.5 }; + } +} + +/** + * Parse a CSS-like position into an array of position keywords among top, bottom, left, right and center + * @param value + * @param [options] + * @param [options.allowCenter=true] allow "center center" + * @param [options.cssOrder=true] force CSS order (y axis then x axis) + */ +export function cleanCssPosition( + value: string | string[], + { allowCenter, cssOrder } = { + allowCenter: true, + cssOrder: true, + } +): [string, string] | null { + if (!value) { + return null; + } + + if (typeof value === 'string') { + value = value.split(' '); + } + + if (value.length === 1) { + if (value[0] === CENTER) { + value = [CENTER, CENTER]; + } else if (X_VALUES.indexOf(value[0]) !== -1) { + value = [CENTER, value[0]]; + } else if (Y_VALUES.indexOf(value[0]) !== -1) { + value = [value[0], CENTER]; + } + } + + if (value.length !== 2 || POS_VALUES.indexOf(value[0]) === -1 || POS_VALUES.indexOf(value[1]) === -1) { + logWarn(`Unparsable position ${value}`); + return null; + } + + if (!allowCenter && value[0] === CENTER && value[1] === CENTER) { + logWarn(`Invalid position center center`); + return null; + } + + if (cssOrder && !cssPositionIsOrdered(value)) { + value = [value[1], value[0]]; + } + if (value[1] === CENTER && X_VALUES.indexOf(value[0]) !== -1) { + value = [CENTER, value[0]]; + } + if (value[0] === CENTER && Y_VALUES.indexOf(value[1]) !== -1) { + value = [value[1], CENTER]; + } + + return value as [string, string]; +} + +/** + * Checks if an array of two positions is ordered (y axis then x axis) + */ +export function cssPositionIsOrdered(value: string[]): boolean { + return Y_VALUES.indexOf(value[0]) !== -1 && X_VALUES.indexOf(value[1]) !== -1; +} + +/** + * @summary Parses an speed + * @param speed in radians/degrees/revolutions per second/minute + * @throws {@link PSVError} when the speed cannot be parsed + */ +export function parseSpeed(speed: string | number): number { + let parsed; + + if (typeof speed === 'string') { + const speedStr = speed.toString().trim(); + + // Speed extraction + let speedValue = parseFloat(speedStr.replace(/^(-?[0-9]+(?:\.[0-9]*)?).*$/, '$1')); + const speedUnit = speedStr.replace(/^-?[0-9]+(?:\.[0-9]*)?(.*)$/, '$1').trim(); + + // "per minute" -> "per second" + if (speedUnit.match(/(pm|per minute)$/)) { + speedValue /= 60; + } + + // Which unit? + switch (speedUnit) { + // Degrees per minute / second + case 'dpm': + case 'degrees per minute': + case 'dps': + case 'degrees per second': + parsed = MathUtils.degToRad(speedValue); + break; + + // Radians per minute / second + case 'rdpm': + case 'radians per minute': + case 'rdps': + case 'radians per second': + parsed = speedValue; + break; + + // Revolutions per minute / second + case 'rpm': + case 'revolutions per minute': + case 'rps': + case 'revolutions per second': + parsed = speedValue * Math.PI * 2; + break; + + // Unknown unit + default: + throw new PSVError(`Unknown speed unit "${speedUnit}"`); + } + } else { + parsed = speed; + } + + return parsed; +} + +/** + * Parses an angle value in radians or degrees and returns a normalized value in radians + * @param angle - eg: 3.14, 3.14rad, 180deg + * @param [zeroCenter=false] - normalize between -Pi - Pi instead of 0 - 2*Pi + * @param [halfCircle=zeroCenter] - normalize between -Pi/2 - Pi/2 instead of -Pi - Pi + * @throws {@link PSVError} when the angle cannot be parsed + */ +export function parseAngle(angle: string | number, zeroCenter = false, halfCircle = zeroCenter): number { + let parsed; + + if (typeof angle === 'string') { + const match = angle + .toLowerCase() + .trim() + .match(/^(-?[0-9]+(?:\.[0-9]*)?)(.*)$/); + + if (!match) { + throw new PSVError(`Unknown angle "${angle}"`); + } + + const value = parseFloat(match[1]); + const unit = match[2]; + + if (unit) { + switch (unit) { + case 'deg': + case 'degs': + parsed = MathUtils.degToRad(value); + break; + case 'rad': + case 'rads': + parsed = value; + break; + default: + throw new PSVError(`Unknown angle unit "${unit}"`); + } + } else { + parsed = value; + } + } else if (typeof angle === 'number' && !isNaN(angle)) { + parsed = angle; + } else { + throw new PSVError(`Unknown angle "${angle}"`); + } + + parsed = wrap(zeroCenter ? parsed + Math.PI : parsed, Math.PI * 2); + + return zeroCenter + ? MathUtils.clamp(parsed - Math.PI, -Math.PI / (halfCircle ? 2 : 1), Math.PI / (halfCircle ? 2 : 1)) + : parsed; +} + +/** + * Creates a THREE texture from an image + */ +export function createTexture(img: HTMLImageElement | HTMLCanvasElement): Texture { + const texture = new Texture(img); + texture.needsUpdate = true; + texture.minFilter = LinearFilter; + texture.generateMipmaps = false; + return texture; +} + +const quaternion = new Quaternion(); + +/** + * Applies the inverse of Euler angles to a vector + */ +export function applyEulerInverse(vector: Vector3, euler: Euler) { + quaternion.setFromEuler(euler).invert(); + vector.applyQuaternion(quaternion); +} + +/** + * Declaration of configuration parsers, used by {@link getConfigParser} + */ +export type ConfigParsers = { + [key in keyof T]: (val: T[key], opts: { defValue: U[key]; rawConfig: T }) => U[key]; +}; + +/** + * Creates a function to validate an user configuration object + * + * @template T type of input config + * @template U type of config after parsing + * + * @param defaults the default configuration + * @param parsers function used to parse/validate the configuration + * + * @example + * ```ts + * type MyConfig = { + * value: number; + * label?: string; + * }; + * + * const getConfig({ + * value: 1, + * label: 'Title', + * }, { + * value(value, { defValue }) { + * return value < 10 ? value : defValue; + * } + * }); + * + * const config = getConfig({ value: 3 }); + * ``` + */ +export function getConfigParser, U extends T = T>( + defaults: Required, + parsers?: ConfigParsers +) { + return (userConfig: T): U => { + if (!userConfig) { + return clone(defaults); + } + + const rawConfig: U = clone({ + ...defaults, + ...userConfig, + }); + + const config: U = {} as U; + + for (let [key, value] of Object.entries(rawConfig) as [keyof T, any][]) { + if (parsers && key in parsers) { + value = parsers[key](value, { + rawConfig: rawConfig, + defValue: defaults[key], + }); + } + else if (!(key in defaults)) { + logWarn(`Unknown option ${key as string}`); + continue; + } + + // @ts-ignore + config[key] = value; + } + + return config; + }; +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/core/tsup.config.js b/packages/core/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/core/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/cubemap-adapter/.typedoc/README.md b/packages/cubemap-adapter/.typedoc/README.md new file mode 100644 index 000000000..26e6ebec8 --- /dev/null +++ b/packages/cubemap-adapter/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/cubemap-adapter](https://www.npmjs.com/package/@photo-sphere-viewer/cubemap-adapter) + +Documentation : https://photo-sphere-viewer.js.org/guide/adapters/cubemap diff --git a/packages/cubemap-adapter/package.json b/packages/cubemap-adapter/package.json new file mode 100644 index 000000000..e16611bda --- /dev/null +++ b/packages/cubemap-adapter/package.json @@ -0,0 +1,25 @@ +{ + "name": "@photo-sphere-viewer/cubemap-adapter", + "version": "0.0.0", + "description": "Photo sphere Viewer adapter to display cubemaps.", + "homepage": "https://photo-sphere-viewer.js.org/guide/adapters/cubemap", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.CubemapAdapter" + }, + "typedoc": { + "displayName": "adapter: Cubemap", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/cubemap-adapter/src/CubemapAdapter.ts b/packages/cubemap-adapter/src/CubemapAdapter.ts new file mode 100644 index 000000000..26d585e4d --- /dev/null +++ b/packages/cubemap-adapter/src/CubemapAdapter.ts @@ -0,0 +1,196 @@ +import type { TextureData, Viewer } from '@photo-sphere-viewer/core'; +import { AbstractAdapter, CONSTANTS, PSVError, SYSTEM, utils } from '@photo-sphere-viewer/core'; +import { BoxGeometry, Mesh, ShaderMaterial, Texture } from 'three'; +import { CubemapAdapterConfig, CubemapPanorama } from './model'; + +type CubemapMesh = Mesh; +type CubemapTexture = TextureData; + +const getConfig = utils.getConfigParser({ + flipTopBottom: false, + blur: false, +}); + +/** + * Adapter for cubemaps + */ +export class CubemapAdapter extends AbstractAdapter { + // PSV faces order is left, front, right, back, top, bottom + // 3JS faces order is left, right, top, bottom, back, front + static readonly CUBE_ARRAY = [0, 2, 4, 5, 3, 1]; + static readonly CUBE_HASHMAP = ['left', 'right', 'top', 'bottom', 'back', 'front']; + + static override readonly id = 'cubemap'; + static override readonly supportsDownload = false; + static override readonly supportsOverlay = true; + + private readonly config: CubemapAdapterConfig; + + constructor(viewer: Viewer, config: CubemapAdapterConfig) { + super(viewer); + + this.config = getConfig(config); + } + + override supportsTransition() { + return true; + } + + override supportsPreload() { + return true; + } + + loadTexture(panorama: CubemapPanorama): Promise { + const cleanPanorama = []; + + if (Array.isArray(panorama)) { + if (panorama.length !== 6) { + return Promise.reject(new PSVError('A cubemap array must contain exactly 6 images.')); + } + + // reorder images + for (let i = 0; i < 6; i++) { + cleanPanorama[i] = panorama[CubemapAdapter.CUBE_ARRAY[i]]; + } + } else if (typeof panorama === 'object') { + if (!CubemapAdapter.CUBE_HASHMAP.every((side) => !!(panorama as any)[side])) { + return Promise.reject( + new PSVError('A cubemap object must contain exactly left, front, right, back, top, bottom images.') + ); + } + + // transform into array + CubemapAdapter.CUBE_HASHMAP.forEach((side, i) => { + cleanPanorama[i] = (panorama as any)[side]; + }); + } else { + return Promise.reject(new PSVError('Invalid cubemap panorama, are you using the right adapter?')); + } + + if (this.viewer.config.fisheye) { + utils.logWarn('fisheye effect with cubemap texture can generate distorsion'); + } + + const promises: Promise[] = []; + const progress = [0, 0, 0, 0, 0, 0]; + + for (let i = 0; i < 6; i++) { + promises.push( + this.viewer.textureLoader + .loadImage(cleanPanorama[i], (p) => { + progress[i] = p; + this.viewer.loader.setProgress(utils.sum(progress) / 6); + }) + .then((img) => this.createCubemapTexture(img)) + ); + } + + return Promise.all(promises).then((texture) => ({ panorama, texture })); + } + + /** + * Creates the final texture from image + */ + private createCubemapTexture(img: HTMLImageElement): Texture { + if (img.width !== img.height) { + utils.logWarn('Invalid base image, the width equal the height'); + } + + // resize image + if (this.config.blur || img.width > SYSTEM.maxTextureWidth) { + const ratio = Math.min(1, SYSTEM.maxCanvasWidth / img.width); + + const buffer = document.createElement('canvas'); + buffer.width = img.width * ratio; + buffer.height = img.height * ratio; + + const ctx = buffer.getContext('2d'); + + if (this.config.blur) { + ctx.filter = 'blur(1px)'; + } + + ctx.drawImage(img, 0, 0, buffer.width, buffer.height); + + return utils.createTexture(buffer); + } + + return utils.createTexture(img); + } + + createMesh(scale = 1): CubemapMesh { + const cubeSize = CONSTANTS.SPHERE_RADIUS * 2 * scale; + const geometry = new BoxGeometry(cubeSize, cubeSize, cubeSize).scale(1, 1, -1) as BoxGeometry; + + const materials = []; + for (let i = 0; i < 6; i++) { + materials.push( + AbstractAdapter.createOverlayMaterial({ + additionalUniforms: { + rotation: { value: 0.0 }, + }, + overrideVertexShader: ` +uniform float rotation; +varying vec2 vUv; +const float mid = 0.5; +void main() { + if (rotation == 0.0) { + vUv = uv; + } else { + vUv = vec2( + cos(rotation) * (uv.x - mid) + sin(rotation) * (uv.y - mid) + mid, + cos(rotation) * (uv.y - mid) - sin(rotation) * (uv.x - mid) + mid + ); + } + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); +}`, + }) + ); + } + + return new Mesh(geometry, materials); + } + + setTexture(mesh: CubemapMesh, textureData: CubemapTexture) { + const { texture } = textureData; + + for (let i = 0; i < 6; i++) { + if (this.config.flipTopBottom && (i === 2 || i === 3)) { + this.__setUniform(mesh, i, 'rotation', Math.PI); + } + + this.__setUniform(mesh, i, AbstractAdapter.OVERLAY_UNIFORMS.panorama, texture[i]); + } + + this.setOverlay(mesh, null, 0); + } + + setOverlay(mesh: CubemapMesh, textureData: CubemapTexture, opacity: number) { + for (let i = 0; i < 6; i++) { + this.__setUniform(mesh, i, AbstractAdapter.OVERLAY_UNIFORMS.overlayOpacity, opacity); + if (!textureData) { + this.__setUniform(mesh, i, AbstractAdapter.OVERLAY_UNIFORMS.overlay, new Texture()); + } else { + this.__setUniform(mesh, i, AbstractAdapter.OVERLAY_UNIFORMS.overlay, textureData.texture[i]); + } + } + } + + setTextureOpacity(mesh: CubemapMesh, opacity: number) { + for (let i = 0; i < 6; i++) { + this.__setUniform(mesh, i, AbstractAdapter.OVERLAY_UNIFORMS.globalOpacity, opacity); + mesh.material[i].transparent = opacity < 1; + } + } + + disposeTexture(textureData: CubemapTexture) { + textureData.texture?.forEach((texture) => texture.dispose()); + } + + private __setUniform(mesh: CubemapMesh, index: number, uniform: string, value: any) { + if (mesh.material[index].uniforms[uniform].value instanceof Texture) { + mesh.material[index].uniforms[uniform].value.dispose(); + } + mesh.material[index].uniforms[uniform].value = value; + } +} diff --git a/packages/cubemap-adapter/src/index.ts b/packages/cubemap-adapter/src/index.ts new file mode 100644 index 000000000..37419a52c --- /dev/null +++ b/packages/cubemap-adapter/src/index.ts @@ -0,0 +1,2 @@ +export { CubemapAdapter } from './CubemapAdapter'; +export * from './model'; diff --git a/packages/cubemap-adapter/src/model.ts b/packages/cubemap-adapter/src/model.ts new file mode 100644 index 000000000..425f4a981 --- /dev/null +++ b/packages/cubemap-adapter/src/model.ts @@ -0,0 +1,30 @@ +/** + * Object defining a cubemap + */ +export type Cubemap = { + left: string; + front: string; + right: string; + back: string; + top: string; + bottom: string; +}; + +/** + * Configuration of a cubemap + * @description if an array, images order is : left, front, right, back, top, bottom + */ +export type CubemapPanorama = Cubemap | string[6]; + +export type CubemapAdapterConfig = { + /** + * set to true if the top and bottom faces are not correctly oriented + * @default false + */ + flipTopBottom?: boolean; + /** + * used for cubemap tiles adapter + * @internal + */ + blur?: boolean; +}; diff --git a/packages/cubemap-adapter/tsconfig.json b/packages/cubemap-adapter/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/cubemap-adapter/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/cubemap-adapter/tsup.config.js b/packages/cubemap-adapter/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/cubemap-adapter/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/cubemap-tiles-adapter/.typedoc/README.md b/packages/cubemap-tiles-adapter/.typedoc/README.md new file mode 100644 index 000000000..56e470de6 --- /dev/null +++ b/packages/cubemap-tiles-adapter/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/cubemap-tiles-adapter](https://www.npmjs.com/package/@photo-sphere-viewer/cubemap-tiles-adapter) + +Documentation : https://photo-sphere-viewer.js.org/guide/adapters/cubemap-tiles diff --git a/packages/cubemap-tiles-adapter/package.json b/packages/cubemap-tiles-adapter/package.json new file mode 100644 index 000000000..8861ac996 --- /dev/null +++ b/packages/cubemap-tiles-adapter/package.json @@ -0,0 +1,27 @@ +{ + "name": "@photo-sphere-viewer/cubemap-tiles-adapter", + "version": "0.0.0", + "description": "Photo sphere Viewer adapter to display tiled cubemaps.", + "homepage": "https://photo-sphere-viewer.js.org/guide/adapters/cubemap-tiles", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0", + "@photo-sphere-viewer/cubemap-adapter": "0.0.0", + "@photo-sphere-viewer/shared": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.CubemapTilesAdapter" + }, + "typedoc": { + "displayName": "adapter: CubemapTiles", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/cubemap-tiles-adapter/src/CubemapTilesAdapter.ts b/packages/cubemap-tiles-adapter/src/CubemapTilesAdapter.ts new file mode 100644 index 000000000..7a8dac5e7 --- /dev/null +++ b/packages/cubemap-tiles-adapter/src/CubemapTilesAdapter.ts @@ -0,0 +1,447 @@ +import type { TextureData, Viewer } from '@photo-sphere-viewer/core'; +import { AbstractAdapter, CONSTANTS, events, PSVError, utils } from '@photo-sphere-viewer/core'; +import { CubemapAdapter } from '@photo-sphere-viewer/cubemap-adapter'; +import { buildErrorMaterial, Queue, Task } from '@photo-sphere-viewer/shared'; +import { + BoxGeometry, + Frustum, + ImageLoader, + MathUtils, + Matrix4, + Mesh, + MeshBasicMaterial, + Texture, + Vector2, + Vector3, +} from 'three'; +import { CubemapTilesAdapterConfig, CubemapTilesPanorama } from './model'; + +type CubemapMesh = Mesh; +type CubemapTexture = TextureData; +type CubemapTile = { face: number; col: number; row: number; angle: number }; + +const CUBE_SEGMENTS = 16; +const NB_VERTICES_BY_FACE = 6; +const NB_VERTICES_BY_PLANE = NB_VERTICES_BY_FACE * CUBE_SEGMENTS * CUBE_SEGMENTS; +const NB_VERTICES = 6 * NB_VERTICES_BY_PLANE; +const NB_GROUPS_BY_FACE = CUBE_SEGMENTS * CUBE_SEGMENTS; + +const ATTR_UV = 'uv'; +const ATTR_ORIGINAL_UV = 'originaluv'; +const ATTR_POSITION = 'position'; + +function tileId(tile: CubemapTile) { + return `${tile.face}:${tile.col}x${tile.row}`; +} + +const getConfig = utils.getConfigParser({ + flipTopBottom: false, + showErrorTile: true, + baseBlur: true, + blur: false, +}); + +const frustum = new Frustum(); +const projScreenMatrix = new Matrix4(); +const vertexPosition = new Vector3(); + +/** + * Adapter for tiled cubemaps + */ +export class CubemapTilesAdapter extends AbstractAdapter { + static override readonly id = 'cubemap-tiles'; + static override readonly supportsDownload = false; + static override readonly supportsOverlay = false; + + private readonly config: CubemapTilesAdapterConfig; + + private readonly state = { + tileSize: 0, + facesByTile: 0, + tiles: {} as Record, + geom: null as BoxGeometry, + materials: [] as MeshBasicMaterial[], + errorMaterial: null as MeshBasicMaterial, + }; + + private adapter: CubemapAdapter; + private readonly queue = new Queue(); + private readonly loader?: ImageLoader; + + constructor(viewer: Viewer, config: CubemapTilesAdapterConfig) { + super(viewer); + + this.config = getConfig(config); + + if (this.viewer.config.requestHeaders) { + utils.logWarn( + 'CubemapTilesAdapter fallbacks to file loader because "requestHeaders" where provided. ' + + 'Consider removing "requestHeaders" if you experience performances issues.' + ); + } else { + this.loader = new ImageLoader(); + if (this.viewer.config.withCredentials) { + this.loader.setWithCredentials(true); + } + } + + this.viewer.addEventListener(events.PositionUpdatedEvent.type, this); + this.viewer.addEventListener(events.ZoomUpdatedEvent.type, this); + } + + override destroy() { + this.viewer.addEventListener(events.PositionUpdatedEvent.type, this); + this.viewer.addEventListener(events.ZoomUpdatedEvent.type, this); + + this.__cleanup(); + + this.state.errorMaterial?.map?.dispose(); + this.state.errorMaterial?.dispose(); + + delete this.state.geom; + delete this.state.errorMaterial; + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof events.PositionUpdatedEvent || e instanceof events.ZoomUpdatedEvent) { + this.__refresh(); + } + } + + override supportsTransition(panorama: CubemapTilesPanorama) { + return !!panorama.baseUrl; + } + + override supportsPreload(panorama: CubemapTilesPanorama) { + return !!panorama.baseUrl; + } + + override loadTexture(panorama: CubemapTilesPanorama): Promise { + if (typeof panorama !== 'object' || !panorama.faceSize || !panorama.nbTiles || !panorama.tileUrl) { + return Promise.reject(new PSVError('Invalid panorama configuration, are you using the right adapter?')); + } + if (panorama.nbTiles > CUBE_SEGMENTS) { + return Promise.reject(new PSVError(`Panorama nbTiles must not be greater than ${CUBE_SEGMENTS}.`)); + } + if (!MathUtils.isPowerOfTwo(panorama.nbTiles)) { + return Promise.reject(new PSVError('Panorama nbTiles must be power of 2.')); + } + + if (panorama.baseUrl) { + if (!this.adapter) { + if (!CubemapAdapter) { + throw new PSVError('CubemapTilesAdapter requires CubemapAdapter'); + } + + this.adapter = new CubemapAdapter(this.viewer, { + blur: this.config.baseBlur, + }); + } + + return this.adapter.loadTexture(panorama.baseUrl).then((textureData) => ({ + panorama: panorama, + texture: textureData.texture, + })); + } else { + return Promise.resolve({ panorama, texture: null }); + } + } + + createMesh(scale = 1): CubemapMesh { + const cubeSize = CONSTANTS.SPHERE_RADIUS * 2 * scale; + const geometry = new BoxGeometry(cubeSize, cubeSize, cubeSize, CUBE_SEGMENTS, CUBE_SEGMENTS, CUBE_SEGMENTS) + .scale(1, 1, -1) + .toNonIndexed() as BoxGeometry; + + geometry.clearGroups(); + for (let i = 0, k = 0; i < NB_VERTICES; i += NB_VERTICES_BY_FACE) { + geometry.addGroup(i, NB_VERTICES_BY_FACE, k++); + } + + geometry.setAttribute(ATTR_ORIGINAL_UV, geometry.getAttribute(ATTR_UV).clone()); + + return new Mesh(geometry, []); + } + + /** + * Applies the base texture and starts the loading of tiles + */ + setTexture(mesh: CubemapMesh, textureData: CubemapTexture, transition: boolean) { + const { panorama, texture } = textureData; + + if (transition) { + this.__setTexture(mesh, texture); + return; + } + + this.__cleanup(); + this.__setTexture(mesh, texture); + + this.state.materials = mesh.material; + this.state.geom = mesh.geometry; + this.state.geom.setAttribute(ATTR_UV, this.state.geom.getAttribute(ATTR_ORIGINAL_UV).clone()); + + this.state.tileSize = panorama.faceSize / panorama.nbTiles; + this.state.facesByTile = CUBE_SEGMENTS / panorama.nbTiles; + + // this.psv.renderer.scene.add(createWireFrame(this.state.geom)); + + setTimeout(() => this.__refresh(true)); + } + + private __setTexture(mesh: CubemapMesh, texture: Texture[]) { + for (let i = 0; i < 6; i++) { + let material; + if (texture) { + if (this.config.flipTopBottom && (i === 2 || i === 3)) { + texture[i].center = new Vector2(0.5, 0.5); + texture[i].rotation = Math.PI; + } + + material = new MeshBasicMaterial({ map: texture[i] }); + } else { + material = new MeshBasicMaterial({ opacity: 0, transparent: true }); + } + + for (let j = 0; j < NB_GROUPS_BY_FACE; j++) { + mesh.material.push(material); + } + } + } + + setTextureOpacity(mesh: CubemapMesh, opacity: number) { + for (let i = 0; i < 6; i++) { + mesh.material[i * NB_GROUPS_BY_FACE].opacity = opacity; + mesh.material[i * NB_GROUPS_BY_FACE].transparent = opacity < 1; + } + } + + /** + * @throws {@link PSVError} always + */ + setOverlay(): void { + throw new PSVError('EquirectangularTilesAdapter does not support overlay'); + } + + disposeTexture(textureData: CubemapTexture) { + textureData.texture?.forEach((texture) => texture.dispose()); + } + + /** + * Compute visible tiles and load them + */ + // @ts-ignore unused paramater + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private __refresh(init = false) { + if (!this.state.geom) { + return; + } + + const camera = this.viewer.renderer.camera; + camera.updateMatrixWorld(); + projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + frustum.setFromProjectionMatrix(projScreenMatrix); + + const panorama: CubemapTilesPanorama = this.viewer.config.panorama; + const verticesPosition = this.state.geom.getAttribute(ATTR_POSITION); + const tilesToLoad: CubemapTile[] = []; + + for (let face = 0; face < 6; face++) { + for (let col = 0; col < panorama.nbTiles; col++) { + for (let row = 0; row < panorama.nbTiles; row++) { + // for each tile, find the vertices corresponding to the four corners + // if at least one vertex is visible, the tile must be loaded + // for larger tiles we also test the four edges centers and the tile center + const verticesIndex = []; + + // top-left + const v0 = face * NB_VERTICES_BY_PLANE + + row * this.state.facesByTile * CUBE_SEGMENTS * NB_VERTICES_BY_FACE + + col * this.state.facesByTile * NB_VERTICES_BY_FACE; + + // bottom-left + const v1 = v0 + CUBE_SEGMENTS * NB_VERTICES_BY_FACE * (this.state.facesByTile - 1) + 1; + + // bottom-right + const v2 = v1 + this.state.facesByTile * NB_VERTICES_BY_FACE - 3; + + // top-right + const v3 = v0 + this.state.facesByTile * NB_VERTICES_BY_FACE - 1; + + verticesIndex.push(v0, v1, v2, v3); + + if (this.state.facesByTile >= CUBE_SEGMENTS / 2) { + // top-center + const v4 = v0 + (this.state.facesByTile / 2) * NB_VERTICES_BY_FACE - 1; + + // bottom-center + const v5 = v1 + (this.state.facesByTile / 2) * NB_VERTICES_BY_FACE - 3; + + // left-center + const v6 = v0 + CUBE_SEGMENTS * NB_VERTICES_BY_FACE * (this.state.facesByTile / 2 - 1) + 1; + + // right-center + const v7 = v6 + this.state.facesByTile * NB_VERTICES_BY_FACE - 3; + + // center-center + const v8 = v6 + (this.state.facesByTile / 2) * NB_VERTICES_BY_FACE; + + verticesIndex.push(v4, v5, v6, v7, v8); + } + + // if (init && face === 5 && col === 0 && row === 0) { + // verticesIndex.forEach((vertexIdx) => { + // this.psv.renderer.scene.add(createDot( + // verticesPosition.getX(vertexIdx), + // verticesPosition.getY(vertexIdx), + // verticesPosition.getZ(vertexIdx) + // )); + // }); + // } + + const vertexVisible = verticesIndex.some((vertexIdx) => { + vertexPosition.set( + verticesPosition.getX(vertexIdx), + verticesPosition.getY(vertexIdx), + verticesPosition.getZ(vertexIdx) + ); + vertexPosition.applyEuler(this.viewer.renderer.sphereCorrection); + return frustum.containsPoint(vertexPosition); + }); + + if (vertexVisible) { + const angle = vertexPosition.angleTo(this.viewer.state.direction); + tilesToLoad.push({ face, col, row, angle }); + } + } + } + } + + this.__loadTiles(tilesToLoad); + } + + /** + * Loads tiles and change existing tiles priority + */ + private __loadTiles(tiles: CubemapTile[]) { + this.queue.disableAllTasks(); + + tiles.forEach((tile) => { + const id = tileId(tile); + + if (this.state.tiles[id]) { + this.queue.setPriority(id, tile.angle); + } else { + this.state.tiles[id] = true; + this.queue.enqueue(new Task(id, tile.angle, (task) => this.__loadTile(tile, task))); + } + }); + + this.queue.start(); + } + + /** + * Loads and draw a tile + */ + private __loadTile(tile: CubemapTile, task: Task): Promise { + const panorama = this.viewer.config.panorama; + + let { col, row } = tile; + if (this.config.flipTopBottom && (tile.face === 2 || tile.face === 3)) { + col = panorama.nbTiles - col - 1; + row = panorama.nbTiles - row - 1; + } + const url = panorama.tileUrl(CubemapAdapter.CUBE_HASHMAP[tile.face], col, row); + + return this.__loadImage(url) + .then((image) => { + if (!task.isCancelled()) { + const material = new MeshBasicMaterial({ map: utils.createTexture(image) }); + this.__swapMaterial(tile.face, tile.col, tile.row, material); + this.viewer.needsUpdate(); + } + }) + .catch(() => { + if (!task.isCancelled() && this.config.showErrorTile) { + if (!this.state.errorMaterial) { + this.state.errorMaterial = buildErrorMaterial(this.state.tileSize, this.state.tileSize); + } + this.__swapMaterial(tile.face, tile.col, tile.row, this.state.errorMaterial); + this.viewer.needsUpdate(); + } + }); + } + + private __loadImage(url: string): Promise { + if (this.loader) { + return new Promise((resolve, reject) => { + this.loader.load(url, resolve, undefined, reject); + }); + } else { + return this.viewer.textureLoader.loadImage(url); + } + } + + /** + * Applies a new texture to the faces + */ + private __swapMaterial(face: number, col: number, row: number, material: MeshBasicMaterial) { + const uvs = this.state.geom.getAttribute(ATTR_UV); + + for (let c = 0; c < this.state.facesByTile; c++) { + for (let r = 0; r < this.state.facesByTile; r++) { + // position of the face (two triangles of the same square) + const faceCol = col * this.state.facesByTile + c; + const faceRow = row * this.state.facesByTile + r; + + // first vertex for this face (6 vertices in total) + const firstVertex = NB_VERTICES_BY_PLANE * face + 6 * (CUBE_SEGMENTS * faceRow + faceCol); + + // swap material + const matIndex = this.state.geom.groups.find((g) => g.start === firstVertex).materialIndex; + this.state.materials[matIndex] = material; + + // define new uvs + let top = 1 - r / this.state.facesByTile; + let bottom = 1 - (r + 1) / this.state.facesByTile; + let left = c / this.state.facesByTile; + let right = (c + 1) / this.state.facesByTile; + + if (this.config.flipTopBottom && (face === 2 || face === 3)) { + top = 1 - top; + bottom = 1 - bottom; + left = 1 - left; + right = 1 - right; + } + + uvs.setXY(firstVertex, left, top); + uvs.setXY(firstVertex + 1, left, bottom); + uvs.setXY(firstVertex + 2, right, top); + uvs.setXY(firstVertex + 3, left, bottom); + uvs.setXY(firstVertex + 4, right, bottom); + uvs.setXY(firstVertex + 5, right, top); + } + } + + uvs.needsUpdate = true; + } + + /** + * Clears loading queue, dispose all materials + */ + private __cleanup() { + this.queue.clear(); + this.state.tiles = {}; + + this.state.materials.forEach((mat) => { + mat?.map?.dispose(); + mat?.dispose(); + }); + this.state.materials.length = 0; + } +} diff --git a/packages/cubemap-tiles-adapter/src/index.ts b/packages/cubemap-tiles-adapter/src/index.ts new file mode 100644 index 000000000..747aba6ec --- /dev/null +++ b/packages/cubemap-tiles-adapter/src/index.ts @@ -0,0 +1,2 @@ +export { CubemapTilesAdapter } from './CubemapTilesAdapter'; +export * from './model'; diff --git a/packages/cubemap-tiles-adapter/src/model.ts b/packages/cubemap-tiles-adapter/src/model.ts new file mode 100644 index 000000000..46494ef3b --- /dev/null +++ b/packages/cubemap-tiles-adapter/src/model.ts @@ -0,0 +1,36 @@ +import type { Cubemap, CubemapAdapterConfig, CubemapPanorama } from '@photo-sphere-viewer/cubemap-adapter'; + +/** + * Configuration of a tiled cubemap + */ +export type CubemapTilesPanorama = { + /** + * low resolution panorama loaded before tiles + */ + baseUrl?: CubemapPanorama; + /** + * size of a face in pixels + */ + faceSize: number; + /** + * number of tiles on a side of a face + */ + nbTiles: number; + /** + * function to build a tile url + */ + tileUrl: (face: keyof Cubemap, col: number, row: number) => string; +}; + +export type CubemapTilesAdapterConfig = CubemapAdapterConfig & { + /** + * shows a warning sign on tiles that cannot be loaded + * @default true + */ + showErrorTile?: boolean; + /** + * applies a blur effect to the low resolution panorama + * @default true + */ + baseBlur?: boolean; +}; diff --git a/packages/cubemap-tiles-adapter/tsconfig.json b/packages/cubemap-tiles-adapter/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/cubemap-tiles-adapter/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/cubemap-tiles-adapter/tsup.config.js b/packages/cubemap-tiles-adapter/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/cubemap-tiles-adapter/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/cubemap-video-adapter/.typedoc/README.md b/packages/cubemap-video-adapter/.typedoc/README.md new file mode 100644 index 000000000..819cb14dc --- /dev/null +++ b/packages/cubemap-video-adapter/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/cubemap-video-adapter](https://www.npmjs.com/package/@photo-sphere-viewer/cubemap-video-adapter) + +Documentation : https://photo-sphere-viewer.js.org/guide/adapters/cubemap-video diff --git a/packages/cubemap-video-adapter/package.json b/packages/cubemap-video-adapter/package.json new file mode 100644 index 000000000..92e99ab2d --- /dev/null +++ b/packages/cubemap-video-adapter/package.json @@ -0,0 +1,26 @@ +{ + "name": "@photo-sphere-viewer/cubemap-video-adapter", + "version": "0.0.0", + "description": "Photo sphere Viewer adapter to display cubemap videos.", + "homepage": "https://photo-sphere-viewer.js.org/guide/adapters/cubemap-video", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0", + "@photo-sphere-viewer/shared": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.CubemapVideoAdapter" + }, + "typedoc": { + "displayName": "adapter: CubemapVideo", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/cubemap-video-adapter/src/CubemapVideoAdapter.ts b/packages/cubemap-video-adapter/src/CubemapVideoAdapter.ts new file mode 100644 index 000000000..a02be5c32 --- /dev/null +++ b/packages/cubemap-video-adapter/src/CubemapVideoAdapter.ts @@ -0,0 +1,160 @@ +import type { TextureData, Viewer } from '@photo-sphere-viewer/core'; +import { CONSTANTS, utils } from '@photo-sphere-viewer/core'; +import { AbstractVideoAdapter } from '@photo-sphere-viewer/shared'; +import { BoxGeometry, Mesh, ShaderMaterial, Vector2, VideoTexture } from 'three'; +import { CubemapVideoAdapterConfig, CubemapVideoPanorama } from './model'; + +type CubemapMesh = Mesh; +type CubemapTexture = TextureData; + +const getConfig = utils.getConfigParser({ + equiangular: true, + autoplay: false, + muted: false, +}); + +/** + * Adapter for cubemap videos + */ +export class CubemapVideoAdapter extends AbstractVideoAdapter { + static override readonly id = 'cubemap-video'; + + protected override readonly config: CubemapVideoAdapterConfig; + + constructor(viewer: Viewer, config: CubemapVideoAdapterConfig) { + super(viewer); + + this.config = getConfig(config); + } + + createMesh(scale = 1): CubemapMesh { + const cubeSize = CONSTANTS.SPHERE_RADIUS * 2 * scale; + const geometry = new BoxGeometry(cubeSize, cubeSize, cubeSize).scale(1, 1, -1).toNonIndexed() as BoxGeometry; + + geometry.clearGroups(); + + const uvs = geometry.getAttribute('uv'); + + /* + Structure of a frame + + 1 +---------+---------+---------+ + | | | | + | Left | Front | Right | + | | | | + 1/2 +---------+---------+---------+ + | | | | + | Bottom | Back | Top | + | | | | + 0 +---------+---------+---------+ + 0 1/3 2/3 1 + + Bottom, Back and Top are rotated 90° clockwise + */ + + // columns + const a = 0; + const b = 1 / 3; + const c = 2 / 3; + const d = 1; + + // lines + const A = 1; + const B = 1 / 2; + const C = 0; + + // left + uvs.setXY(0, a, A); + uvs.setXY(1, a, B); + uvs.setXY(2, b, A); + uvs.setXY(3, a, B); + uvs.setXY(4, b, B); + uvs.setXY(5, b, A); + + // right + uvs.setXY(6, c, A); + uvs.setXY(7, c, B); + uvs.setXY(8, d, A); + uvs.setXY(9, c, B); + uvs.setXY(10, d, B); + uvs.setXY(11, d, A); + + // top + uvs.setXY(12, d, B); + uvs.setXY(13, c, B); + uvs.setXY(14, d, C); + uvs.setXY(15, c, B); + uvs.setXY(16, c, C); + uvs.setXY(17, d, C); + + // bottom + uvs.setXY(18, b, B); + uvs.setXY(19, a, B); + uvs.setXY(20, b, C); + uvs.setXY(21, a, B); + uvs.setXY(22, a, C); + uvs.setXY(23, b, C); + + // back + uvs.setXY(24, c, B); + uvs.setXY(25, b, B); + uvs.setXY(26, c, C); + uvs.setXY(27, b, B); + uvs.setXY(28, b, C); + uvs.setXY(29, c, C); + + // front + uvs.setXY(30, b, A); + uvs.setXY(31, b, B); + uvs.setXY(32, c, A); + uvs.setXY(33, b, B); + uvs.setXY(34, c, B); + uvs.setXY(35, c, A); + + // shamelessly copied from https://github.com/videojs/videojs-vr + const material = new ShaderMaterial({ + uniforms: { + mapped: { value: null }, + contCorrect: { value: 1 }, + faceWH: { value: new Vector2(1 / 3, 1 / 2) }, + vidWH: { value: new Vector2(1, 1) }, + }, + vertexShader: ` +varying vec2 vUv; +void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.); +}`, + fragmentShader: ` +varying vec2 vUv; +uniform sampler2D mapped; +uniform vec2 faceWH; +uniform vec2 vidWH; +uniform float contCorrect; + +const float PI = 3.1415926535897932384626433832795; + +void main() { + vec2 corner = vUv - mod(vUv, faceWH) + vec2(0, contCorrect / vidWH.y); + vec2 faceWHadj = faceWH - vec2(0, contCorrect * 2. / vidWH.y); + vec2 p = (vUv - corner) / faceWHadj - .5; + vec2 q = ${this.config.equiangular ? '2. / PI * atan(2. * p) + .5' : 'p + .5'}; + vec2 eUv = corner + q * faceWHadj; + gl_FragColor = texture2D(mapped, eUv); +}`, + }); + + return new Mesh(geometry, material); + } + + setTexture(mesh: CubemapMesh, textureData: CubemapTexture) { + const { texture } = textureData; + const video: HTMLVideoElement = texture.image; + + mesh.material.uniforms.mapped.value?.dispose(); + mesh.material.uniforms.mapped.value = texture; + mesh.material.uniforms.vidWH.value.set(video.videoWidth, video.videoHeight); + + this.switchVideo(textureData.texture); + } +} diff --git a/packages/cubemap-video-adapter/src/index.ts b/packages/cubemap-video-adapter/src/index.ts new file mode 100644 index 000000000..e2f1d5abf --- /dev/null +++ b/packages/cubemap-video-adapter/src/index.ts @@ -0,0 +1,2 @@ +export { CubemapVideoAdapter } from './CubemapVideoAdapter'; +export * from './model'; diff --git a/packages/cubemap-video-adapter/src/model.ts b/packages/cubemap-video-adapter/src/model.ts new file mode 100644 index 000000000..b70aca01a --- /dev/null +++ b/packages/cubemap-video-adapter/src/model.ts @@ -0,0 +1,14 @@ +import type { AbstractVideoAdapterConfig, AbstractVideoPanorama } from '@photo-sphere-viewer/shared'; + +/** + * Configuration of a cubemap video + */ +export type CubemapVideoPanorama = AbstractVideoPanorama; + +export type CubemapVideoAdapterConfig = AbstractVideoAdapterConfig & { + /** + * if the video is an equiangular cubemap (EAC) + * @default true + */ + equiangular?: boolean; +}; diff --git a/packages/cubemap-video-adapter/tsconfig.json b/packages/cubemap-video-adapter/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/cubemap-video-adapter/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/cubemap-video-adapter/tsup.config.js b/packages/cubemap-video-adapter/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/cubemap-video-adapter/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/equirectangular-tiles-adapter/.typedoc/README.md b/packages/equirectangular-tiles-adapter/.typedoc/README.md new file mode 100644 index 000000000..7c8c0489e --- /dev/null +++ b/packages/equirectangular-tiles-adapter/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/equirectangular-tiles-adapter](https://www.npmjs.com/package/@photo-sphere-viewer/equirectangular-tiles-adapter) + +Documentation : https://photo-sphere-viewer.js.org/guide/adapters/equirectangular-tiles diff --git a/packages/equirectangular-tiles-adapter/package.json b/packages/equirectangular-tiles-adapter/package.json new file mode 100644 index 000000000..5af228b74 --- /dev/null +++ b/packages/equirectangular-tiles-adapter/package.json @@ -0,0 +1,26 @@ +{ + "name": "@photo-sphere-viewer/equirectangular-tiles-adapter", + "version": "0.0.0", + "description": "Photo sphere Viewer adapter to display tiled panoramas.", + "homepage": "https://photo-sphere-viewer.js.org/guide/adapters/equirectangular-tiles", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0", + "@photo-sphere-viewer/shared": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.EquirectangularTilesAdapter" + }, + "typedoc": { + "displayName": "adapter: EquirectangularTiles", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/equirectangular-tiles-adapter/src/EquirectangularTilesAdapter.ts b/packages/equirectangular-tiles-adapter/src/EquirectangularTilesAdapter.ts new file mode 100644 index 000000000..647506e35 --- /dev/null +++ b/packages/equirectangular-tiles-adapter/src/EquirectangularTilesAdapter.ts @@ -0,0 +1,613 @@ +import type { TextureData, Viewer } from '@photo-sphere-viewer/core'; +import { AbstractAdapter, CONSTANTS, EquirectangularAdapter, events, PSVError, utils } from '@photo-sphere-viewer/core'; +import { buildErrorMaterial, Queue, Task } from '@photo-sphere-viewer/shared'; +import { + Frustum, + ImageLoader, + MathUtils, + Matrix4, + Mesh, + MeshBasicMaterial, + SphereGeometry, + Texture, + Vector3, +} from 'three'; +import { EquirectangularTilesAdapterConfig, EquirectangularTilesPanorama } from './model'; + +/* the faces of the top and bottom rows are made of a single triangle (3 vertices) + * all other faces are made of two triangles (6 vertices) + * bellow is the indexing of each face vertices + * + * first row faces: + * ⋀ + * /0\ + * / \ + * / \ + * /1 2\ + * ¯¯¯¯¯¯¯¯¯ + * + * other rows faces: + * _________ + * |\1 0| + * |3\ | + * | \ | + * | \ | + * | \ | + * | \2| + * |4 5\| + * ¯¯¯¯¯¯¯¯¯ + * + * last row faces: + * _________ + * \1 0/ + * \ / + * \ / + * \2/ + * ⋁ + */ + +type EquirectangularMesh = Mesh; +type EquirectangularTexture = TextureData; +type EquirectangularTile = { col: number; row: number; angle: number }; + +const NB_VERTICES_BY_FACE = 6; +const NB_VERTICES_BY_SMALL_FACE = 3; + +const ATTR_UV = 'uv'; +const ATTR_ORIGINAL_UV = 'originaluv'; +const ATTR_POSITION = 'position'; + +function tileId(tile: EquirectangularTile): string { + return `${tile.col}x${tile.row}`; +} + +const getConfig = utils.getConfigParser( + { + resolution: 64, + showErrorTile: true, + baseBlur: true, + blur: false, + }, + { + resolution: (resolution) => { + if (!resolution || !MathUtils.isPowerOfTwo(resolution)) { + throw new PSVError('EquirectangularTilesAdapter resolution must be power of two'); + } + return resolution; + }, + } +); + +const frustum = new Frustum(); +const projScreenMatrix = new Matrix4(); +const vertexPosition = new Vector3(); + +/** + * Adapter for tiled panoramas + */ +export class EquirectangularTilesAdapter extends AbstractAdapter { + static override readonly id = 'equirectangular-tiles'; + static override readonly supportsDownload = false; + static override readonly supportsOverlay = false; + + private readonly SPHERE_SEGMENTS: number; + private readonly SPHERE_HORIZONTAL_SEGMENTS: number; + private readonly NB_VERTICES: number; + private readonly NB_GROUPS: number; + + private readonly config: EquirectangularTilesAdapterConfig; + + private readonly state = { + colSize: 0, + rowSize: 0, + facesByCol: 0, + facesByRow: 0, + tiles: {} as Record, + geom: null as SphereGeometry, + materials: [] as MeshBasicMaterial[], + errorMaterial: null as MeshBasicMaterial, + }; + + private adapter: EquirectangularAdapter; + private readonly queue = new Queue(); + private readonly loader?: ImageLoader; + + constructor(viewer: Viewer, config: EquirectangularTilesAdapterConfig) { + super(viewer); + + this.config = getConfig(config); + + this.viewer.config.useXmpData = false; + + this.SPHERE_SEGMENTS = this.config.resolution; + this.SPHERE_HORIZONTAL_SEGMENTS = this.SPHERE_SEGMENTS / 2; + this.NB_VERTICES = 2 * this.SPHERE_SEGMENTS * NB_VERTICES_BY_SMALL_FACE + + (this.SPHERE_HORIZONTAL_SEGMENTS - 2) * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE; + this.NB_GROUPS = this.SPHERE_SEGMENTS * this.SPHERE_HORIZONTAL_SEGMENTS; + + if (this.viewer.config.requestHeaders) { + utils.logWarn( + 'EquirectangularTilesAdapter fallbacks to file loader because "requestHeaders" where provided. ' + + 'Consider removing "requestHeaders" if you experience performances issues.' + ); + } else { + this.loader = new ImageLoader(); + if (this.viewer.config.withCredentials) { + this.loader.setWithCredentials(true); + } + } + + this.viewer.addEventListener(events.PositionUpdatedEvent.type, this); + this.viewer.addEventListener(events.ZoomUpdatedEvent.type, this); + } + + override destroy() { + this.viewer.addEventListener(events.PositionUpdatedEvent.type, this); + this.viewer.addEventListener(events.ZoomUpdatedEvent.type, this); + + this.__cleanup(); + + this.state.errorMaterial?.map?.dispose(); + this.state.errorMaterial?.dispose(); + + delete this.state.geom; + delete this.state.errorMaterial; + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof events.PositionUpdatedEvent || e instanceof events.ZoomUpdatedEvent) { + this.__refresh(); + } + } + + override supportsTransition(panorama: EquirectangularTilesPanorama) { + return !!panorama.baseUrl; + } + + override supportsPreload(panorama: EquirectangularTilesPanorama) { + return !!panorama.baseUrl; + } + + override loadTexture(panorama: EquirectangularTilesPanorama): Promise { + if (typeof panorama !== 'object' || !panorama.width || !panorama.cols || !panorama.rows || !panorama.tileUrl) { + return Promise.reject(new PSVError('Invalid panorama configuration, are you using the right adapter?')); + } + if (panorama.cols > this.SPHERE_SEGMENTS) { + return Promise.reject(new PSVError(`Panorama cols must not be greater than ${this.SPHERE_SEGMENTS}.`)); + } + if (panorama.rows > this.SPHERE_HORIZONTAL_SEGMENTS) { + return Promise.reject( + new PSVError(`Panorama rows must not be greater than ${this.SPHERE_HORIZONTAL_SEGMENTS}.`) + ); + } + if (!MathUtils.isPowerOfTwo(panorama.cols) || !MathUtils.isPowerOfTwo(panorama.rows)) { + return Promise.reject(new PSVError('Panorama cols and rows must be powers of 2.')); + } + + const panoData = { + fullWidth: panorama.width, + fullHeight: panorama.width / 2, + croppedWidth: panorama.width, + croppedHeight: panorama.width / 2, + croppedX: 0, + croppedY: 0, + poseHeading: 0, + posePitch: 0, + poseRoll: 0, + }; + + if (panorama.baseUrl) { + if (!this.adapter) { + this.adapter = new EquirectangularAdapter(this.viewer, { + blur: this.config.baseBlur, + }); + } + + return this.adapter.loadTexture(panorama.baseUrl, panorama.basePanoData).then((textureData) => ({ + panorama: panorama, + texture: textureData.texture, + panoData: panoData, + })); + } else { + return Promise.resolve({ panorama, panoData, texture: null }); + } + } + + createMesh(scale = 1): EquirectangularMesh { + const geometry = new SphereGeometry( + CONSTANTS.SPHERE_RADIUS * scale, + this.SPHERE_SEGMENTS, + this.SPHERE_HORIZONTAL_SEGMENTS, + -Math.PI / 2 + ) + .scale(-1, 1, 1) + .toNonIndexed() as SphereGeometry; + + geometry.clearGroups(); + let i = 0; + let k = 0; + // first row + for (; i < this.SPHERE_SEGMENTS * NB_VERTICES_BY_SMALL_FACE; i += NB_VERTICES_BY_SMALL_FACE) { + geometry.addGroup(i, NB_VERTICES_BY_SMALL_FACE, k++); + } + // second to before last rows + for (; i < this.NB_VERTICES - this.SPHERE_SEGMENTS * NB_VERTICES_BY_SMALL_FACE; i += NB_VERTICES_BY_FACE) { + geometry.addGroup(i, NB_VERTICES_BY_FACE, k++); + } + // last row + for (; i < this.NB_VERTICES; i += NB_VERTICES_BY_SMALL_FACE) { + geometry.addGroup(i, NB_VERTICES_BY_SMALL_FACE, k++); + } + + geometry.setAttribute(ATTR_ORIGINAL_UV, geometry.getAttribute(ATTR_UV).clone()); + + return new Mesh(geometry, []); + } + + /** + * Applies the base texture and starts the loading of tiles + */ + setTexture(mesh: EquirectangularMesh, textureData: EquirectangularTexture, transition: boolean) { + const { panorama, texture } = textureData; + + if (transition) { + this.__setTexture(mesh, texture); + return; + } + + this.__cleanup(); + this.__setTexture(mesh, texture); + + this.state.materials = mesh.material; + this.state.geom = mesh.geometry; + this.state.geom.setAttribute(ATTR_UV, this.state.geom.getAttribute(ATTR_ORIGINAL_UV).clone()); + + this.state.colSize = panorama.width / panorama.cols; + this.state.rowSize = panorama.width / 2 / panorama.rows; + this.state.facesByCol = this.SPHERE_SEGMENTS / panorama.cols; + this.state.facesByRow = this.SPHERE_HORIZONTAL_SEGMENTS / panorama.rows; + + // this.psv.renderer.scene.add(createWireFrame(this.state.geom)); + + setTimeout(() => this.__refresh(true)); + } + + private __setTexture(mesh: EquirectangularMesh, texture: Texture) { + let material; + if (texture) { + material = new MeshBasicMaterial({ map: texture }); + } else { + material = new MeshBasicMaterial({ opacity: 0, transparent: true }); + } + + for (let i = 0; i < this.NB_GROUPS; i++) { + mesh.material.push(material); + } + } + + setTextureOpacity(mesh: EquirectangularMesh, opacity: number) { + mesh.material[0].opacity = opacity; + mesh.material[0].transparent = opacity < 1; + } + + /** + * @throws {@link PSVError} always + */ + setOverlay() { + throw new PSVError('EquirectangularTilesAdapter does not support overlay'); + } + + disposeTexture(textureData: TextureData) { + textureData.texture?.dispose(); + } + + /** + * Compute visible tiles and load them + */ + // @ts-ignore unused paramater + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private __refresh(init = false) { + if (!this.state.geom) { + return; + } + + const camera = this.viewer.renderer.camera; + camera.updateMatrixWorld(); + projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); + frustum.setFromProjectionMatrix(projScreenMatrix); + + const panorama: EquirectangularTilesPanorama = this.viewer.config.panorama; + const verticesPosition = this.state.geom.getAttribute(ATTR_POSITION); + const tilesToLoad = []; + + for (let col = 0; col < panorama.cols; col++) { + for (let row = 0; row < panorama.rows; row++) { + // for each tile, find the vertices corresponding to the four corners (three for first and last rows) + // if at least one vertex is visible, the tile must be loaded + // for larger tiles we also test the four edges centers and the tile center + + const verticesIndex = []; + + if (row === 0) { + // bottom-left + const v0 = this.state.facesByRow === 1 + ? col * this.state.facesByCol * NB_VERTICES_BY_SMALL_FACE + 1 + : this.SPHERE_SEGMENTS * NB_VERTICES_BY_SMALL_FACE + + (this.state.facesByRow - 2) * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE + + col * this.state.facesByCol * NB_VERTICES_BY_FACE + 4; + + // bottom-right + const v1 = this.state.facesByRow === 1 + ? v0 + (this.state.facesByCol - 1) * NB_VERTICES_BY_SMALL_FACE + 1 + : v0 + (this.state.facesByCol - 1) * NB_VERTICES_BY_FACE + 1; + + // top (all vertices are equal) + const v2 = 0; + + verticesIndex.push(v0, v1, v2); + + if (this.state.facesByCol >= this.SPHERE_SEGMENTS / 8) { + // bottom-center + const v4 = v0 + this.state.facesByCol / 2 * NB_VERTICES_BY_FACE; + + verticesIndex.push(v4); + } + + if (this.state.facesByRow >= this.SPHERE_HORIZONTAL_SEGMENTS / 4) { + // left-center + const v6 = v0 - this.state.facesByRow / 2 * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE; + + // right-center + const v7 = v1 - this.state.facesByRow / 2 * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE; + + verticesIndex.push(v6, v7); + } + } else if (row === panorama.rows - 1) { + // top-left + const v0 = this.state.facesByRow === 1 + ? -this.SPHERE_SEGMENTS * NB_VERTICES_BY_SMALL_FACE + + row * this.state.facesByRow * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE + + col * this.state.facesByCol * NB_VERTICES_BY_SMALL_FACE + 1 + : -this.SPHERE_SEGMENTS * NB_VERTICES_BY_SMALL_FACE + + row * this.state.facesByRow * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE + + col * this.state.facesByCol * NB_VERTICES_BY_FACE + 1; + + // top-right + const v1 = this.state.facesByRow === 1 + ? v0 + (this.state.facesByCol - 1) * NB_VERTICES_BY_SMALL_FACE - 1 + : v0 + (this.state.facesByCol - 1) * NB_VERTICES_BY_FACE - 1; + + // bottom (all vertices are equal) + const v2 = this.NB_VERTICES - 1; + + verticesIndex.push(v0, v1, v2); + + if (this.state.facesByCol >= this.SPHERE_SEGMENTS / 8) { + // top-center + const v4 = v0 + this.state.facesByCol / 2 * NB_VERTICES_BY_FACE; + + verticesIndex.push(v4); + } + + if (this.state.facesByRow >= this.SPHERE_HORIZONTAL_SEGMENTS / 4) { + // left-center + const v6 = v0 + this.state.facesByRow / 2 * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE; + + // right-center + const v7 = v1 + this.state.facesByRow / 2 * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE; + + verticesIndex.push(v6, v7); + } + } else { + // top-left + const v0 = -this.SPHERE_SEGMENTS * NB_VERTICES_BY_SMALL_FACE + + row * this.state.facesByRow * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE + + col * this.state.facesByCol * NB_VERTICES_BY_FACE + 1; + + // bottom-left + const v1 = v0 + (this.state.facesByRow - 1) * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE + 3; + + // bottom-right + const v2 = v1 + (this.state.facesByCol - 1) * NB_VERTICES_BY_FACE + 1; + + // top-right + const v3 = v0 + (this.state.facesByCol - 1) * NB_VERTICES_BY_FACE - 1; + + verticesIndex.push(v0, v1, v2, v3); + + if (this.state.facesByCol >= this.SPHERE_SEGMENTS / 8) { + // top-center + const v4 = v0 + this.state.facesByCol / 2 * NB_VERTICES_BY_FACE; + + // bottom-center + const v5 = v1 + this.state.facesByCol / 2 * NB_VERTICES_BY_FACE; + + verticesIndex.push(v4, v5); + } + + if (this.state.facesByRow >= this.SPHERE_HORIZONTAL_SEGMENTS / 4) { + // left-center + const v6 = v0 + this.state.facesByRow / 2 * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE; + + // right-center + const v7 = v3 + this.state.facesByRow / 2 * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE; + + verticesIndex.push(v6, v7); + + if (this.state.facesByCol >= this.SPHERE_SEGMENTS / 8) { + // center-center + const v8 = v6 + this.state.facesByCol / 2 * NB_VERTICES_BY_FACE; + + verticesIndex.push(v8); + } + } + } + + // if (init && col === 0 && row === 0) { + // verticesIndex.forEach((vertexIdx) => { + // this.psv.renderer.scene.add(createDot( + // verticesPosition.getX(vertexIdx), + // verticesPosition.getY(vertexIdx), + // verticesPosition.getZ(vertexIdx) + // )); + // }); + // } + + const vertexVisible = verticesIndex.some((vertexIdx) => { + vertexPosition.set( + verticesPosition.getX(vertexIdx), + verticesPosition.getY(vertexIdx), + verticesPosition.getZ(vertexIdx) + ); + vertexPosition.applyEuler(this.viewer.renderer.sphereCorrection); + return frustum.containsPoint(vertexPosition); + }); + + if (vertexVisible) { + let angle = vertexPosition.angleTo(this.viewer.state.direction); + if (row === 0 || row === panorama.rows - 1) { + angle *= 2; // lower priority to top and bottom tiles + } + tilesToLoad.push({ col, row, angle }); + } + } + } + + this.__loadTiles(tilesToLoad); + } + + /** + * Loads tiles and change existing tiles priority + */ + private __loadTiles(tiles: EquirectangularTile[]) { + this.queue.disableAllTasks(); + + tiles.forEach((tile) => { + const id = tileId(tile); + + if (this.state.tiles[id]) { + this.queue.setPriority(id, tile.angle); + } else { + this.state.tiles[id] = true; + this.queue.enqueue(new Task(id, tile.angle, (task) => this.__loadTile(tile, task))); + } + }); + + this.queue.start(); + } + + /** + * Loads and draw a tile + */ + private __loadTile(tile: EquirectangularTile, task: Task): Promise { + const panorama: EquirectangularTilesPanorama = this.viewer.config.panorama; + const url = panorama.tileUrl(tile.col, tile.row); + + return this.__loadImage(url) + .then((image) => { + if (!task.isCancelled()) { + const material = new MeshBasicMaterial({ map: utils.createTexture(image) }); + this.__swapMaterial(tile.col, tile.row, material); + this.viewer.needsUpdate(); + } + }) + .catch(() => { + if (!task.isCancelled() && this.config.showErrorTile) { + if (!this.state.errorMaterial) { + this.state.errorMaterial = buildErrorMaterial(this.state.colSize, this.state.rowSize); + } + this.__swapMaterial(tile.col, tile.row, this.state.errorMaterial); + this.viewer.needsUpdate(); + } + }); + } + + private __loadImage(url: string): Promise { + if (this.loader) { + return new Promise((resolve, reject) => { + this.loader.load(url, resolve, undefined, reject); + }); + } else { + return this.viewer.textureLoader.loadImage(url); + } + } + + /** + * Applies a new texture to the faces + */ + private __swapMaterial(col: number, row: number, material: MeshBasicMaterial) { + const uvs = this.state.geom.getAttribute(ATTR_UV); + + for (let c = 0; c < this.state.facesByCol; c++) { + for (let r = 0; r < this.state.facesByRow; r++) { + // position of the face (two triangles of the same square) + const faceCol = col * this.state.facesByCol + c; + const faceRow = row * this.state.facesByRow + r; + const isFirstRow = faceRow === 0; + const isLastRow = faceRow === (this.SPHERE_HORIZONTAL_SEGMENTS - 1); + + // first vertex for this face (3 or 6 vertices in total) + let firstVertex: number; + if (isFirstRow) { + firstVertex = faceCol * NB_VERTICES_BY_SMALL_FACE; + } else if (isLastRow) { + firstVertex = this.NB_VERTICES + - this.SPHERE_SEGMENTS * NB_VERTICES_BY_SMALL_FACE + + faceCol * NB_VERTICES_BY_SMALL_FACE; + } else { + firstVertex = this.SPHERE_SEGMENTS * NB_VERTICES_BY_SMALL_FACE + + (faceRow - 1) * this.SPHERE_SEGMENTS * NB_VERTICES_BY_FACE + + faceCol * NB_VERTICES_BY_FACE; + } + + // swap material + const matIndex = this.state.geom.groups.find((g) => g.start === firstVertex).materialIndex; + this.state.materials[matIndex] = material; + + // define new uvs + const top = 1 - r / this.state.facesByRow; + const bottom = 1 - (r + 1) / this.state.facesByRow; + const left = c / this.state.facesByCol; + const right = (c + 1) / this.state.facesByCol; + + if (isFirstRow) { + uvs.setXY(firstVertex, (left + right) / 2, top); + uvs.setXY(firstVertex + 1, left, bottom); + uvs.setXY(firstVertex + 2, right, bottom); + } else if (isLastRow) { + uvs.setXY(firstVertex, right, top); + uvs.setXY(firstVertex + 1, left, top); + uvs.setXY(firstVertex + 2, (left + right) / 2, bottom); + } else { + uvs.setXY(firstVertex, right, top); + uvs.setXY(firstVertex + 1, left, top); + uvs.setXY(firstVertex + 2, right, bottom); + uvs.setXY(firstVertex + 3, left, top); + uvs.setXY(firstVertex + 4, left, bottom); + uvs.setXY(firstVertex + 5, right, bottom); + } + } + } + + uvs.needsUpdate = true; + } + + /** + * Clears loading queue, dispose all materials + */ + private __cleanup() { + this.queue.clear(); + this.state.tiles = {}; + + this.state.materials.forEach((mat) => { + mat?.map?.dispose(); + mat?.dispose(); + }); + this.state.materials.length = 0; + } +} diff --git a/packages/equirectangular-tiles-adapter/src/index.ts b/packages/equirectangular-tiles-adapter/src/index.ts new file mode 100644 index 000000000..3b82786a4 --- /dev/null +++ b/packages/equirectangular-tiles-adapter/src/index.ts @@ -0,0 +1,2 @@ +export { EquirectangularTilesAdapter } from './EquirectangularTilesAdapter'; +export * from './model'; diff --git a/packages/equirectangular-tiles-adapter/src/model.ts b/packages/equirectangular-tiles-adapter/src/model.ts new file mode 100644 index 000000000..4364e8494 --- /dev/null +++ b/packages/equirectangular-tiles-adapter/src/model.ts @@ -0,0 +1,44 @@ +import type { EquirectangularAdapterConfig, PanoData, PanoDataProvider } from '@photo-sphere-viewer/core'; + +/** + * Configuration of a tiled panorama + */ +export type EquirectangularTilesPanorama = { + /** + * low resolution panorama loaded before tiles + */ + baseUrl?: string; + /** + * panoData configuration associated to low resolution panorama loaded before tiles + */ + basePanoData?: PanoData | PanoDataProvider; + /** + * complete panorama width (height is always width/2) + */ + width: number; + /** + * number of vertical tiles (must be a power of 2) + */ + cols: number; + /** + * number of horizontal tiles (must be a power of 2) + */ + rows: number; + /** + * function to build a tile url + */ + tileUrl: (col: number, row: number) => string; +}; + +export type EquirectangularTilesAdapterConfig = EquirectangularAdapterConfig & { + /** + * shows a warning sign on tiles that cannot be loaded + * @default true + */ + showErrorTile?: boolean; + /** + * applies a blur effect to the low resolution panorama + * @default true + */ + baseBlur?: boolean; +}; diff --git a/packages/equirectangular-tiles-adapter/tsconfig.json b/packages/equirectangular-tiles-adapter/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/equirectangular-tiles-adapter/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/equirectangular-tiles-adapter/tsup.config.js b/packages/equirectangular-tiles-adapter/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/equirectangular-tiles-adapter/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/equirectangular-video-adapter/.typedoc/README.md b/packages/equirectangular-video-adapter/.typedoc/README.md new file mode 100644 index 000000000..53e5d6078 --- /dev/null +++ b/packages/equirectangular-video-adapter/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/equirectangular-video-adapter](https://www.npmjs.com/package/@photo-sphere-viewer/equirectangular-video-adapter) + +Documentation : https://photo-sphere-viewer.js.org/guide/adapters/equirectangular-video diff --git a/packages/equirectangular-video-adapter/package.json b/packages/equirectangular-video-adapter/package.json new file mode 100644 index 000000000..468b4d7db --- /dev/null +++ b/packages/equirectangular-video-adapter/package.json @@ -0,0 +1,26 @@ +{ + "name": "@photo-sphere-viewer/equirectangular-video-adapter", + "version": "0.0.0", + "description": "Photo sphere Viewer adapter to display equirectangular videos.", + "homepage": "https://photo-sphere-viewer.js.org/guide/adapters/equirectangular-video", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0", + "@photo-sphere-viewer/shared": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.EquirectangularVideoAdapter" + }, + "typedoc": { + "displayName": "adapter: EquirectangularVideo", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/equirectangular-video-adapter/src/EquirectangularVideoAdapter.ts b/packages/equirectangular-video-adapter/src/EquirectangularVideoAdapter.ts new file mode 100644 index 000000000..dffa5c6e1 --- /dev/null +++ b/packages/equirectangular-video-adapter/src/EquirectangularVideoAdapter.ts @@ -0,0 +1,85 @@ +import type { TextureData, Viewer } from '@photo-sphere-viewer/core'; +import { CONSTANTS, PSVError, utils } from '@photo-sphere-viewer/core'; +import { AbstractVideoAdapter } from '@photo-sphere-viewer/shared'; +import { MathUtils, Mesh, MeshBasicMaterial, SphereGeometry, VideoTexture } from 'three'; +import { EquirectangularVideoAdapterConfig, EquirectangularVideoPanorama } from './model'; + +type EquirectangularMesh = Mesh; +type EquirectangularTexture = TextureData; + +const getConfig = utils.getConfigParser( + { + resolution: 64, + autoplay: false, + muted: false, + blur: false, + }, + { + resolution: (resolution) => { + if (!resolution || !MathUtils.isPowerOfTwo(resolution)) { + throw new PSVError('EquirectangularTilesAdapter resolution must be power of two'); + } + return resolution; + }, + } +); + +/** + * Adapter for equirectangular videos + */ +export class EquirectangularVideoAdapter extends AbstractVideoAdapter { + static override readonly id = 'equirectangular-video'; + + protected override readonly config: EquirectangularVideoAdapterConfig; + + private readonly SPHERE_SEGMENTS: number; + private readonly SPHERE_HORIZONTAL_SEGMENTS: number; + + constructor(viewer: Viewer, config: EquirectangularVideoAdapterConfig) { + super(viewer); + + this.config = getConfig(config); + + this.SPHERE_SEGMENTS = this.config.resolution; + this.SPHERE_HORIZONTAL_SEGMENTS = this.SPHERE_SEGMENTS / 2; + } + + override loadTexture(panorama: EquirectangularVideoPanorama): Promise { + return super.loadTexture(panorama).then(({ texture }) => { + const video: HTMLVideoElement = texture.image; + const panoData = { + fullWidth: video.videoWidth, + fullHeight: video.videoHeight, + croppedWidth: video.videoWidth, + croppedHeight: video.videoHeight, + croppedX: 0, + croppedY: 0, + poseHeading: 0, + posePitch: 0, + poseRoll: 0, + }; + + return { panorama, texture, panoData }; + }); + } + + createMesh(scale = 1): EquirectangularMesh { + const geometry = new SphereGeometry( + CONSTANTS.SPHERE_RADIUS * scale, + this.SPHERE_SEGMENTS, + this.SPHERE_HORIZONTAL_SEGMENTS, + -Math.PI / 2 + ).scale(-1, 1, 1) as SphereGeometry; + + const material = new MeshBasicMaterial(); + + return new Mesh(geometry, material); + } + + setTexture(mesh: EquirectangularMesh, textureData: EquirectangularTexture) { + mesh.material.map?.dispose(); + mesh.material.map = textureData.texture; + + this.switchVideo(textureData.texture); + } +} diff --git a/packages/equirectangular-video-adapter/src/index.ts b/packages/equirectangular-video-adapter/src/index.ts new file mode 100644 index 000000000..89cea1d1a --- /dev/null +++ b/packages/equirectangular-video-adapter/src/index.ts @@ -0,0 +1,2 @@ +export { EquirectangularVideoAdapter } from './EquirectangularVideoAdapter'; +export * from './model'; diff --git a/packages/equirectangular-video-adapter/src/model.ts b/packages/equirectangular-video-adapter/src/model.ts new file mode 100644 index 000000000..37f11fd4a --- /dev/null +++ b/packages/equirectangular-video-adapter/src/model.ts @@ -0,0 +1,9 @@ +import type { EquirectangularAdapterConfig } from '@photo-sphere-viewer/core'; +import type { AbstractVideoAdapterConfig, AbstractVideoPanorama } from '@photo-sphere-viewer/shared'; + +/** + * Configuration of an equirectangular video + */ +export type EquirectangularVideoPanorama = AbstractVideoPanorama; + +export type EquirectangularVideoAdapterConfig = EquirectangularAdapterConfig & AbstractVideoAdapterConfig; diff --git a/packages/equirectangular-video-adapter/tsconfig.json b/packages/equirectangular-video-adapter/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/equirectangular-video-adapter/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/equirectangular-video-adapter/tsup.config.js b/packages/equirectangular-video-adapter/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/equirectangular-video-adapter/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/gallery-plugin/.typedoc/README.md b/packages/gallery-plugin/.typedoc/README.md new file mode 100644 index 000000000..15213937b --- /dev/null +++ b/packages/gallery-plugin/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/gallery-plugin](https://www.npmjs.com/package/@photo-sphere-viewer/gallery-plugin) + +Documentation : https://photo-sphere-viewer.js.org/plugins/gallery diff --git a/packages/gallery-plugin/package.json b/packages/gallery-plugin/package.json new file mode 100644 index 000000000..94db619c0 --- /dev/null +++ b/packages/gallery-plugin/package.json @@ -0,0 +1,26 @@ +{ + "name": "@photo-sphere-viewer/gallery-plugin", + "version": "0.0.0", + "description": "Photo sphere Viewer plugin to add a gallery of multiple panoramas.", + "homepage": "https://photo-sphere-viewer.js.org/plugins/gallery", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix && stylelint \"src/**/*.scss\" --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.GalleryPlugin", + "style": true + }, + "typedoc": { + "displayName": "plugin: Gallery", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/gallery-plugin/src/GalleryButton.ts b/packages/gallery-plugin/src/GalleryButton.ts new file mode 100644 index 000000000..8bfb93056 --- /dev/null +++ b/packages/gallery-plugin/src/GalleryButton.ts @@ -0,0 +1,59 @@ +import { AbstractButton } from '@photo-sphere-viewer/core'; +import type { Navbar } from '@photo-sphere-viewer/core'; +import type { GalleryPlugin } from './GalleryPlugin'; +import gallery from './icons/gallery.svg'; +import { HideGalleryEvent, ShowGalleryEvent } from './events'; + +export class GalleryButton extends AbstractButton { + static override readonly id = 'gallery'; + + private readonly plugin: GalleryPlugin; + + /** + * @param {PSV.components.Navbar} navbar + */ + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-gallery-button', + hoverScale: true, + collapsable: true, + tabbable: true, + icon: gallery, + }); + + this.plugin = this.viewer.getPlugin('gallery'); + + if (this.plugin) { + this.plugin.addEventListener(ShowGalleryEvent.type, this); + this.plugin.addEventListener(HideGalleryEvent.type, this); + } + } + + override destroy() { + if (this.plugin) { + this.plugin.removeEventListener(ShowGalleryEvent.type, this); + this.plugin.removeEventListener(HideGalleryEvent.type, this); + } + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof ShowGalleryEvent) { + this.toggleActive(true); + } else if (e instanceof HideGalleryEvent) { + this.toggleActive(false); + } + } + + override isSupported() { + return !!this.plugin; + } + + onClick() { + this.plugin.toggle(); + } +} diff --git a/packages/gallery-plugin/src/GalleryComponent.ts b/packages/gallery-plugin/src/GalleryComponent.ts new file mode 100644 index 000000000..5a7143056 --- /dev/null +++ b/packages/gallery-plugin/src/GalleryComponent.ts @@ -0,0 +1,144 @@ +import type { Viewer } from '@photo-sphere-viewer/core'; +import { AbstractComponent, utils } from '@photo-sphere-viewer/core'; +import { ACTIVE_CLASS, GALLERY_ITEM_DATA, GALLERY_ITEM_DATA_KEY, ITEMS_TEMPLATE } from './constants'; +import type { GalleryPlugin } from './GalleryPlugin'; +import blankIcon from './icons/blank.svg'; +import { GalleryItem } from './model'; + +export class GalleryComponent extends AbstractComponent { + + protected override readonly state = { + visible: true, + mousedown: false, + initMouseX: null as number, + mouseX: null as number, + }; + + private readonly observer: IntersectionObserver; + private readonly items: HTMLElement; + + constructor(private readonly plugin: GalleryPlugin, viewer: Viewer) { + super(viewer, { + className: 'psv-gallery psv--capture-event', + }); + + this.container.innerHTML = blankIcon; + this.container.querySelector('svg').style.display = 'none'; + + const closeBtn = document.createElement('div'); + closeBtn.className = 'psv-panel-close-button'; + this.container.appendChild(closeBtn); + + this.items = document.createElement('div'); + this.items.className = 'psv-gallery-container'; + this.container.appendChild(this.items); + + this.observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.intersectionRatio > 0) { + const element = entry.target as HTMLElement; + element.style.backgroundImage = `url(${element.dataset.src})`; + delete element.dataset.src; + this.observer.unobserve(entry.target); + } + }); + }, + { + root: this.viewer.container, + } + ); + + this.container.addEventListener('wheel', this); + this.container.addEventListener('mousedown', this); + this.container.addEventListener('mousemove', this); + this.container.addEventListener('click', this); + window.addEventListener('mouseup', this); + + closeBtn.addEventListener('click', () => this.plugin.hide()); + + this.hide(); + } + + override destroy() { + window.removeEventListener('mouseup', this); + + this.observer.disconnect(); + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + switch (e.type) { + case 'wheel': + this.container.scrollLeft += (e as WheelEvent).deltaY * 50; + e.preventDefault(); + break; + + case 'mousedown': + this.state.mousedown = true; + this.state.initMouseX = (e as MouseEvent).clientX; + this.state.mouseX = (e as MouseEvent).clientX; + break; + + case 'mousemove': + if (this.state.mousedown) { + const delta = this.state.mouseX - (e as MouseEvent).clientX; + this.container.scrollLeft += delta; + this.state.mouseX = (e as MouseEvent).clientX; + } + break; + + case 'mouseup': + this.state.mousedown = false; + this.state.mouseX = null; + e.preventDefault(); + break; + + case 'click': + // prevent click on drag + if (Math.abs(this.state.initMouseX - (e as MouseEvent).clientX) < 10) { + const item = utils.getClosest(e.target as HTMLElement, `[data-${GALLERY_ITEM_DATA_KEY}]`); + if (item) { + this.plugin.__click(item.dataset[GALLERY_ITEM_DATA]); + } + } + break; + } + } + + override show() { + this.container.classList.add('psv-gallery--open'); + this.state.visible = true; + } + + override hide() { + this.container.classList.remove('psv-gallery--open'); + this.state.visible = false; + } + + setItems(items: GalleryItem[]) { + this.items.innerHTML = ITEMS_TEMPLATE(items, this.plugin.config.thumbnailSize); + + if (this.observer) { + this.observer.disconnect(); + + this.items.querySelectorAll('[data-src]').forEach((child) => { + this.observer.observe(child); + }); + } + } + + setActive(id: GalleryItem['id']) { + const currentActive = this.items.querySelector('.' + ACTIVE_CLASS); + currentActive?.classList.remove(ACTIVE_CLASS); + + if (id) { + const nextActive = this.items.querySelector(`[data-${GALLERY_ITEM_DATA_KEY}="${id}"]`); + nextActive?.classList.add(ACTIVE_CLASS); + } + } +} diff --git a/packages/gallery-plugin/src/GalleryPlugin.ts b/packages/gallery-plugin/src/GalleryPlugin.ts new file mode 100644 index 000000000..8dc0327ba --- /dev/null +++ b/packages/gallery-plugin/src/GalleryPlugin.ts @@ -0,0 +1,167 @@ +import type { Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin, events, PSVError, utils } from '@photo-sphere-viewer/core'; +import { GalleryPluginEvents, HideGalleryEvent, ShowGalleryEvent } from './events'; +import { GalleryButton } from './GalleryButton'; +import { GalleryComponent } from './GalleryComponent'; +import { GalleryItem, GalleryPluginConfig } from './model'; + +const getConfig = utils.getConfigParser({ + items: [], + visibleOnLoad: false, + hideOnClick: true, + thumbnailSize: { + width: 200, + height: 100, + }, +}); + +/** + * Adds a gallery of multiple panoramas + */ +export class GalleryPlugin extends AbstractPlugin { + static override readonly id = 'gallery'; + + readonly config: GalleryPluginConfig; + + private readonly gallery: GalleryComponent; + + private items: GalleryItem[] = []; + private handler?: (id: GalleryItem['id']) => void; + private currentId?: GalleryItem['id']; + + constructor(viewer: Viewer, config: GalleryPluginConfig) { + super(viewer); + + this.config = getConfig(config); + + this.gallery = new GalleryComponent(this, this.viewer); + } + + /** + * @internal + */ + override init() { + super.init(); + + this.viewer.addEventListener(events.PanoramaLoadedEvent.type, this); + this.viewer.addEventListener(events.ShowPanelEvent.type, this); + + if (this.config.visibleOnLoad) { + this.viewer.addEventListener(events.ReadyEvent.type, () => { + this.show(); + }, { once: true }); + } + + this.setItems(this.config.items); + delete this.config.items; + } + + /** + * @internal + */ + override destroy() { + this.viewer.removeEventListener(events.PanoramaLoadedEvent.type, this); + this.viewer.removeEventListener(events.ShowPanelEvent.type, this); + + this.gallery.destroy(); + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof events.PanoramaLoadedEvent) { + const item = this.items.find((i) => utils.deepEqual(i.panorama, e.data.panorama)); + this.currentId = item?.id; + this.gallery.setActive(item?.id); + } else if (e instanceof events.ShowPanelEvent) { + this.gallery.isVisible() && this.hide(); + } + } + + /** + * Shows the gallery + */ + show() { + this.dispatchEvent(new ShowGalleryEvent()); + return this.gallery.show(); + } + + /** + * Hides the carousem + */ + hide() { + this.dispatchEvent(new HideGalleryEvent()); + return this.gallery.hide(); + } + + /** + * Hides or shows the gallery + */ + toggle() { + if (this.gallery.isVisible()) { + this.hide(); + } else { + this.show(); + } + } + + /** + * Sets the list of items + * @param items + * @param [handler] function that will be called when an item is clicked instead of the default behavior + * @throws {@link PSVError} if the configuration is invalid + */ + setItems(items: GalleryItem[], handler?: (id: GalleryItem['id']) => void) { + if (!items) { + items = []; + } else { + items.forEach((item, i) => { + if (!item.id) { + throw new PSVError(`Item ${i} has no "id".`); + } + if (!item.panorama) { + throw new PSVError(`Item ${item.id} has no "panorama".`); + } + }); + } + + this.handler = handler; + this.items = items.map((item) => ({ + ...item, + id: `${item.id}`, + })); + + this.gallery.setItems(this.items); + + this.viewer.navbar.getButton(GalleryButton.id, false)?.toggle(this.items.length > 0); + } + + /** + * @internal + */ + __click(id: GalleryItem['id']) { + if (id === this.currentId) { + return; + } + + if (this.handler) { + this.handler(id); + } else { + const item = this.items.find((i) => i.id === id); + this.viewer.setPanorama(item.panorama, { + caption: item.name, + ...item.options, + }); + } + + this.currentId = id; + this.gallery.setActive(id); + + if (this.config.hideOnClick) { + this.hide(); + } + } +} diff --git a/packages/gallery-plugin/src/constants.ts b/packages/gallery-plugin/src/constants.ts new file mode 100644 index 000000000..9c2c84446 --- /dev/null +++ b/packages/gallery-plugin/src/constants.ts @@ -0,0 +1,37 @@ +import type { Size } from '@photo-sphere-viewer/core'; +import { utils } from '@photo-sphere-viewer/core'; +import { GalleryItem } from './model'; + +/** + * Property name added to gallery items + * @internal + */ +export const GALLERY_ITEM_DATA = 'psvGalleryItem'; + +/** + * Property name added to gallery items (dash-case) + * @internal + */ +export const GALLERY_ITEM_DATA_KEY = utils.dasherize(GALLERY_ITEM_DATA); + +/** + * Class added to active gallery items + * @internal + */ +export const ACTIVE_CLASS = 'psv-gallery-item--active'; + +/** + * Gallery template + * @internal + */ +export const ITEMS_TEMPLATE = (items: GalleryItem[], size: Size) => ` +${items.map((item) => ` + +`).join('')} +`; diff --git a/packages/gallery-plugin/src/events.ts b/packages/gallery-plugin/src/events.ts new file mode 100644 index 000000000..314720923 --- /dev/null +++ b/packages/gallery-plugin/src/events.ts @@ -0,0 +1,26 @@ +import { TypedEvent } from '@photo-sphere-viewer/core'; +import type { GalleryPlugin } from './GalleryPlugin'; + +/** + * @event Triggered when the gallery shown + */ +export class ShowGalleryEvent extends TypedEvent { + static override readonly type = 'show-gallery'; + + constructor() { + super(ShowGalleryEvent.type); + } +} + +/** + * @event Triggered when the gallery hidden + */ +export class HideGalleryEvent extends TypedEvent { + static override readonly type = 'hide-gallery'; + + constructor() { + super(ShowGalleryEvent.type); + } +} + +export type GalleryPluginEvents = ShowGalleryEvent | HideGalleryEvent; diff --git a/src/plugins/gallery/blank.svg b/packages/gallery-plugin/src/icons/blank.svg similarity index 99% rename from src/plugins/gallery/blank.svg rename to packages/gallery-plugin/src/icons/blank.svg index a77924cf6..2f7256fc0 100644 --- a/src/plugins/gallery/blank.svg +++ b/packages/gallery-plugin/src/icons/blank.svg @@ -9,4 +9,4 @@ - + \ No newline at end of file diff --git a/src/plugins/gallery/gallery.svg b/packages/gallery-plugin/src/icons/gallery.svg similarity index 89% rename from src/plugins/gallery/gallery.svg rename to packages/gallery-plugin/src/icons/gallery.svg index e4924f7c1..32690cb2e 100644 --- a/src/plugins/gallery/gallery.svg +++ b/packages/gallery-plugin/src/icons/gallery.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/packages/gallery-plugin/src/index.ts b/packages/gallery-plugin/src/index.ts new file mode 100644 index 000000000..21744b907 --- /dev/null +++ b/packages/gallery-plugin/src/index.ts @@ -0,0 +1,13 @@ +import { DEFAULTS, registerButton } from '@photo-sphere-viewer/core'; +import * as events from './events'; +import { GalleryButton } from './GalleryButton'; + +DEFAULTS.lang[GalleryButton.id] = 'Gallery'; +registerButton(GalleryButton, 'caption:left'); + +export { GalleryPlugin } from './GalleryPlugin'; +export * from './model'; +export { events }; + +/** @internal */ +import './style.scss'; diff --git a/packages/gallery-plugin/src/model.ts b/packages/gallery-plugin/src/model.ts new file mode 100644 index 000000000..b3e6fef6d --- /dev/null +++ b/packages/gallery-plugin/src/model.ts @@ -0,0 +1,43 @@ +import type { PanoramaOptions, Size } from '@photo-sphere-viewer/core'; + +export type GalleryItem = { + /** + * Unique identifier of the item + */ + id: string | number; + /** + * Panorama of the item + */ + panorama: any; + /** + * URL of the thumbnail + */ + thumbnail?: string; + /** + * Text visible over the thumbnail + */ + name?: string; + /** + * Any option supported by the `setPanorama()` method + */ + options?: PanoramaOptions; +}; + +export type GalleryPluginConfig = { + items?: GalleryItem[]; + /** + * Displays the gallery when loading the first panorama + * @default false + */ + visibleOnLoad?: boolean; + /** + * Hides the gallery when the user clicks on an item + * @default true + */ + hideOnClick?: boolean; + /** + * Size of thumbnails + * @default 200x100 + */ + thumbnailSize?: Size; +}; diff --git a/packages/gallery-plugin/src/style.scss b/packages/gallery-plugin/src/style.scss new file mode 100644 index 000000000..c0ac7e331 --- /dev/null +++ b/packages/gallery-plugin/src/style.scss @@ -0,0 +1,137 @@ +@import '../../shared/src/vars'; + +$psv-gallery-padding: 15px !default; +$psv-gallery-border: 1px solid $psv-caption-text-color !default; +$psv-gallery-background: $psv-navbar-background !default; +$psv-gallery-item-radius: 5px !default; +$psv-gallery-item-active-border: 3px solid white !default; +$psv-gallery-title-font: $psv-caption-font !default; +$psv-gallery-title-color: $psv-caption-text-color !default; +$psv-gallery-title-background: rgba(0, 0, 0, 0.6) !default; +$psv-gallery-thumb-hover-scale: 1.2 !default; + +.psv-gallery { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + background: $psv-gallery-background; + border-bottom: $psv-gallery-border; + overflow-x: auto; + overflow-y: hidden; + transition: transform ease-in-out 0.1s; + transform: translateY(100%); + z-index: $psv-navbar-zindex; + + @at-root .psv--has-navbar & { + bottom: $psv-navbar-height; + transform: translateY(calc(100% + #{$psv-navbar-height})); + } + + &--open { + transform: translateY(0) !important; + } + + &-container { + display: flex; + padding: $psv-gallery-padding; + } + + &-item { + flex: none; + position: relative; + margin-right: $psv-gallery-padding; + border-radius: $psv-gallery-item-radius; + overflow: hidden; + cursor: pointer; + + &-wrapper { + width: 100%; + height: 0; + } + + &-title { + position: absolute; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: flex-start; + box-sizing: border-box; + width: 100%; + height: 2.2em; + padding: 0.5em; + background: $psv-gallery-title-background; + font: $psv-gallery-title-font; + line-height: 1.2em; + color: $psv-gallery-title-color; + z-index: 2; + transition: height ease-in-out 0.2s; + + span { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + user-select: none; + } + } + + &-thumb { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-position: center center; + background-size: cover; + transform: scale3d(1, 1, 1); + transition: transform ease-in-out 0.2s; + z-index: 1; + } + + &:hover &-title { + height: 100%; + + span { + white-space: normal; + } + } + + &:hover &-thumb { + transform: scale3d($psv-gallery-thumb-hover-scale, $psv-gallery-thumb-hover-scale, 1); + } + + &--active::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + box-sizing: border-box; + border: $psv-gallery-item-active-border; + z-index: 3; + } + } + + @media screen and (max-width: 500px) { + top: 0; + overflow-x: hidden; + overflow-y: auto; + + &-container { + flex-wrap: wrap; + margin-right: -$psv-gallery-padding; + } + + &-item { + width: calc(50% - #{$psv-gallery-padding}) !important; + margin-bottom: $psv-gallery-padding; + } + + .psv-panel-close-button { + display: block; + z-index: 10; + } + } +} diff --git a/packages/gallery-plugin/tsconfig.json b/packages/gallery-plugin/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/gallery-plugin/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/gallery-plugin/tsup.config.js b/packages/gallery-plugin/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/gallery-plugin/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/gyroscope-plugin/.typedoc/README.md b/packages/gyroscope-plugin/.typedoc/README.md new file mode 100644 index 000000000..a2b56229b --- /dev/null +++ b/packages/gyroscope-plugin/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/gyroscope-plugin](https://www.npmjs.com/package/@photo-sphere-viewer/gyroscope-plugin) + +Documentation : https://photo-sphere-viewer.js.org/plugins/gyroscope diff --git a/packages/gyroscope-plugin/package.json b/packages/gyroscope-plugin/package.json new file mode 100644 index 000000000..db1cb4419 --- /dev/null +++ b/packages/gyroscope-plugin/package.json @@ -0,0 +1,25 @@ +{ + "name": "@photo-sphere-viewer/gyroscope-plugin", + "version": "0.0.0", + "description": "Photo sphere Viewer plugin to add gyroscope controls on mobile devices.", + "homepage": "https://photo-sphere-viewer.js.org/plugins/gyroscope", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.GyroscopePlugin" + }, + "typedoc": { + "displayName": "plugin: Gyroscope", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/gyroscope-plugin/src/DeviceOrientationControls.d.ts b/packages/gyroscope-plugin/src/DeviceOrientationControls.d.ts new file mode 100644 index 000000000..cd0833fb4 --- /dev/null +++ b/packages/gyroscope-plugin/src/DeviceOrientationControls.d.ts @@ -0,0 +1,15 @@ +import { Object3D } from 'three'; + +export class DeviceOrientationControls { + deviceOrientation: any; + screenOrientation: number; + alphaOffset: number; + + constructor(public object: Object3D); + + connect(); + + disconnect(); + + update(); +} diff --git a/packages/gyroscope-plugin/src/DeviceOrientationControls.js b/packages/gyroscope-plugin/src/DeviceOrientationControls.js new file mode 100644 index 000000000..fcc9024ae --- /dev/null +++ b/packages/gyroscope-plugin/src/DeviceOrientationControls.js @@ -0,0 +1,118 @@ +import { Euler, MathUtils, Quaternion, Vector3 } from 'three'; + +const _zee = new Vector3(0, 0, 1); +const _euler = new Euler(); +const _q0 = new Quaternion(); +const _q1 = new Quaternion(-Math.sqrt(0.5), 0, 0, Math.sqrt(0.5)); // - PI/2 around the x-axis + +/** + * Copied from three.js examples before deletion in r134 + * (deleted because of constructors/OS inconsistencies) + * @internal + */ +export class DeviceOrientationControls { + constructor(object) { + if (window.isSecureContext === false) { + console.error( + 'THREE.DeviceOrientationControls: DeviceOrientationEvent is only available in secure contexts (https)' + ); + } + + const scope = this; + + const EPS = 0.000001; + const lastQuaternion = new Quaternion(); + + this.object = object; + this.object.rotation.reorder('YXZ'); + + this.enabled = true; + + this.deviceOrientation = {}; + this.screenOrientation = 0; + + this.alphaOffset = 0; // radians + + const onDeviceOrientationChangeEvent = function (event) { + scope.deviceOrientation = event; + }; + + const onScreenOrientationChangeEvent = function () { + scope.screenOrientation = window.orientation || 0; + }; + + // The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y'' + + const setObjectQuaternion = function (quaternion, alpha, beta, gamma, orient) { + _euler.set(beta, alpha, -gamma, 'YXZ'); // 'ZXY' for the device, but 'YXZ' for us + + quaternion.setFromEuler(_euler); // orient the device + + quaternion.multiply(_q1); // camera looks out the back of the device, not the top + + quaternion.multiply(_q0.setFromAxisAngle(_zee, -orient)); // adjust for screen orientation + }; + + this.connect = function () { + onScreenOrientationChangeEvent(); // run once on load + + // iOS 13+ + + if ( + window.DeviceOrientationEvent !== undefined + && typeof window.DeviceOrientationEvent.requestPermission === 'function' + ) { + window.DeviceOrientationEvent.requestPermission() + .then(function (response) { + if (response == 'granted') { + window.addEventListener('orientationchange', onScreenOrientationChangeEvent); + window.addEventListener('deviceorientation', onDeviceOrientationChangeEvent); + } + }) + .catch(function (error) { + console.error('THREE.DeviceOrientationControls: Unable to use DeviceOrientation API:', error); + }); + } else { + window.addEventListener('orientationchange', onScreenOrientationChangeEvent); + window.addEventListener('deviceorientation', onDeviceOrientationChangeEvent); + } + + scope.enabled = true; + }; + + this.disconnect = function () { + window.removeEventListener('orientationchange', onScreenOrientationChangeEvent); + window.removeEventListener('deviceorientation', onDeviceOrientationChangeEvent); + + scope.enabled = false; + }; + + this.update = function () { + if (scope.enabled === false) return; + + const device = scope.deviceOrientation; + + if (device) { + const alpha = device.alpha ? MathUtils.degToRad(device.alpha) + scope.alphaOffset : 0; // Z + + const beta = device.beta ? MathUtils.degToRad(device.beta) : 0; // X' + + const gamma = device.gamma ? MathUtils.degToRad(device.gamma) : 0; // Y'' + + const orient = scope.screenOrientation ? MathUtils.degToRad(scope.screenOrientation) : 0; // O + + setObjectQuaternion(scope.object.quaternion, alpha, beta, gamma, orient); + + if (8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { + lastQuaternion.copy(scope.object.quaternion); + } + } + }; + + this.dispose = function () { + scope.disconnect(); + }; + + this.connect(); + } +} diff --git a/packages/gyroscope-plugin/src/GyroscopeButton.ts b/packages/gyroscope-plugin/src/GyroscopeButton.ts new file mode 100644 index 000000000..c21cd66e9 --- /dev/null +++ b/packages/gyroscope-plugin/src/GyroscopeButton.ts @@ -0,0 +1,58 @@ +import type { Navbar } from '@photo-sphere-viewer/core'; +import { AbstractButton } from '@photo-sphere-viewer/core'; +import compass from './compass.svg'; +import { GyroscopeUpdatedEvent } from './events'; +import type { GyroscopePlugin } from './GyroscopePlugin'; + +export class GyroscopeButton extends AbstractButton { + static override readonly id = 'gyroscope'; + + private readonly plugin: GyroscopePlugin; + + /** + * @param {PSV.components.Navbar} navbar + */ + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-gyroscope-button', + icon: compass, + hoverScale: true, + collapsable: true, + tabbable: true, + }); + + this.plugin = this.viewer.getPlugin('gyroscope'); + + if (this.plugin) { + this.plugin.addEventListener(GyroscopeUpdatedEvent.type, this); + } + } + + override destroy() { + if (this.plugin) { + this.plugin.removeEventListener(GyroscopeUpdatedEvent.type, this); + } + + super.destroy(); + } + + override isSupported() { + return !this.plugin ? false : { initial: false, promise: this.plugin.isSupported() }; + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof GyroscopeUpdatedEvent) { + this.toggleActive(e.gyroscopeEnabled); + } + } + + /** + * Toggles gyroscope control + */ + onClick() { + this.plugin.toggle(); + } +} diff --git a/packages/gyroscope-plugin/src/GyroscopePlugin.ts b/packages/gyroscope-plugin/src/GyroscopePlugin.ts new file mode 100644 index 000000000..fe1761cdc --- /dev/null +++ b/packages/gyroscope-plugin/src/GyroscopePlugin.ts @@ -0,0 +1,268 @@ +import type { Position, Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin, events, utils } from '@photo-sphere-viewer/core'; +import { Object3D, Vector3 } from 'three'; +import { DeviceOrientationControls } from './DeviceOrientationControls'; +import { GyroscopePluginEvents, GyroscopeUpdatedEvent } from './events'; +import { GyroscopePluginConfig } from './model.js'; + +const getConfig = utils.getConfigParser( + { + touchmove: true, + absolutePosition: false, + moveMode: 'smooth', + }, + { + moveMode(moveMode, { defValue }) { + if (moveMode !== 'smooth' && moveMode !== 'fast') { + utils.logWarn(`GyroscopePlugin: invalid moveMode`); + return defValue; + } else { + return moveMode; + } + }, + } +); + +const direction = new Vector3(); + +/** + * Adds gyroscope controls on mobile devices + */ +export class GyroscopePlugin extends AbstractPlugin { + static override readonly id = 'gyroscope'; + + readonly config: GyroscopePluginConfig; + + private readonly state = { + isSupported: this.__checkSupport(), + alphaOffset: 0, + enabled: false, + config_moveInertia: true, + }; + + private controls: DeviceOrientationControls; + + constructor(viewer: Viewer, config: GyroscopePluginConfig) { + super(viewer); + + this.config = getConfig(config); + } + + /** + * @internal + */ + override init() { + super.init(); + + this.viewer.addEventListener(events.StopAllEvent.type, this); + this.viewer.addEventListener(events.BeforeRotateEvent.type, this); + this.viewer.addEventListener(events.BeforeRenderEvent.type, this); + } + + /** + * @internal + */ + override destroy() { + this.viewer.removeEventListener(events.StopAllEvent.type, this); + this.viewer.removeEventListener(events.BeforeRotateEvent.type, this); + this.viewer.removeEventListener(events.BeforeRenderEvent.type, this); + + this.stop(); + + delete this.controls; + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof events.StopAllEvent) { + this.stop(); + } else if (e instanceof events.BeforeRenderEvent) { + this.__onBeforeRender(); + } else if (e instanceof events.BeforeRotateEvent) { + this.__onBeforeRotate(e as events.BeforeRotateEvent); + } + } + + /** + * Checks if the gyroscope is supported + */ + isSupported(): Promise { + return this.state.isSupported; + } + + /** + * Checks if the gyroscope is enabled + */ + isEnabled(): boolean { + return this.state.enabled; + } + + /** + * Enables the gyroscope navigation if available + */ + start(): Promise { + return this.state.isSupported + .then((supported) => { + if (supported) { + return this.__requestPermission(); + } else { + utils.logWarn('gyroscope not available'); + return Promise.reject(); + } + }) + .then((granted) => { + if (granted) { + return Promise.resolve(); + } else { + utils.logWarn('gyroscope not allowed'); + return Promise.reject(); + } + }) + .then(() => { + this.viewer.stopAll(); + + // disable inertia + this.state.config_moveInertia = this.viewer.config.moveInertia; + this.viewer.config.moveInertia = false; + + // enable gyro controls + if (!this.controls) { + this.controls = new DeviceOrientationControls(new Object3D()); + } else { + this.controls.connect(); + } + + // force reset + this.controls.deviceOrientation = null; + this.controls.screenOrientation = 0; + this.controls.alphaOffset = 0; + + this.state.alphaOffset = this.config.absolutePosition ? 0 : null; + this.state.enabled = true; + + this.dispatchEvent(new GyroscopeUpdatedEvent(true)); + }); + } + + /** + * Disables the gyroscope navigation + */ + stop() { + if (this.isEnabled()) { + this.controls.disconnect(); + + this.state.enabled = false; + this.viewer.config.moveInertia = this.state.config_moveInertia; + + this.dispatchEvent(new GyroscopeUpdatedEvent(false)); + + this.viewer.resetIdleTimer(); + } + } + + /** + * Enables or disables the gyroscope navigation + */ + toggle() { + if (this.isEnabled()) { + this.stop(); + } else { + this.start(); + } + } + + /** + * Handles gyro movements + */ + private __onBeforeRender() { + if (!this.isEnabled()) { + return; + } + + if (!this.controls.deviceOrientation) { + return; + } + + const position = this.viewer.getPosition(); + + // on first run compute the offset depending on the current viewer position and device orientation + if (this.state.alphaOffset === null) { + this.controls.update(); + this.controls.object.getWorldDirection(direction); + + const sphericalCoords = this.viewer.dataHelper.vector3ToSphericalCoords(direction); + this.state.alphaOffset = sphericalCoords.yaw - position.yaw; + } else { + this.controls.alphaOffset = this.state.alphaOffset; + this.controls.update(); + this.controls.object.getWorldDirection(direction); + + const sphericalCoords = this.viewer.dataHelper.vector3ToSphericalCoords(direction); + + const target: Position = { + yaw: sphericalCoords.yaw, + pitch: -sphericalCoords.pitch, + }; + + // having a slow speed on smalls movements allows to absorb the device/hand vibrations + const step = this.config.moveMode === 'smooth' ? 3 : 10; + this.viewer.dynamics.position.goto(target, utils.getAngle(position, target) < 0.01 ? 1 : step); + } + } + + /** + * Intercepts moves and offsets the alpha angle + */ + private __onBeforeRotate(e: events.BeforeRotateEvent) { + if (this.isEnabled()) { + e.preventDefault(); + + if (this.config.touchmove) { + this.state.alphaOffset -= e.position.yaw - this.viewer.getPosition().pitch; + } + } + } + + /** + * Detects if device orientation is supported + */ + private __checkSupport(): Promise { + if ( + 'DeviceOrientationEvent' in window + && typeof (DeviceOrientationEvent as any).requestPermission === 'function' + ) { + return Promise.resolve(true); + } else if ('DeviceOrientationEvent' in window) { + return new Promise((resolve) => { + const listener = (e: DeviceOrientationEvent) => { + resolve(e && e.alpha !== null && !isNaN(e.alpha)); + + window.removeEventListener('deviceorientation', listener); + }; + + window.addEventListener('deviceorientation', listener, false); + setTimeout(listener, 10000); + }); + } else { + return Promise.resolve(false); + } + } + + /** + * Request permission to the motion API + */ + private __requestPermission(): Promise { + if (typeof (DeviceOrientationEvent as any).requestPermission === 'function') { + return (DeviceOrientationEvent as any) + .requestPermission() + .then((response: string) => response === 'granted') + .catch(() => false); + } else { + return Promise.resolve(true); + } + } +} diff --git a/src/plugins/gyroscope/compass.svg b/packages/gyroscope-plugin/src/compass.svg similarity index 99% rename from src/plugins/gyroscope/compass.svg rename to packages/gyroscope-plugin/src/compass.svg index a2587a963..a53cb7108 100644 --- a/src/plugins/gyroscope/compass.svg +++ b/packages/gyroscope-plugin/src/compass.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/packages/gyroscope-plugin/src/events.ts b/packages/gyroscope-plugin/src/events.ts new file mode 100644 index 000000000..0c0d4a55d --- /dev/null +++ b/packages/gyroscope-plugin/src/events.ts @@ -0,0 +1,15 @@ +import { TypedEvent } from '@photo-sphere-viewer/core'; +import type { GyroscopePlugin } from './GyroscopePlugin'; + +/** + * Triggered when the gyroscope control is enabled/disabled + */ +export class GyroscopeUpdatedEvent extends TypedEvent { + static override readonly type = 'gyroscope-updated'; + + constructor(public readonly gyroscopeEnabled: boolean) { + super(GyroscopeUpdatedEvent.type); + } +} + +export type GyroscopePluginEvents = GyroscopeUpdatedEvent; diff --git a/packages/gyroscope-plugin/src/index.ts b/packages/gyroscope-plugin/src/index.ts new file mode 100644 index 000000000..92a7fde5c --- /dev/null +++ b/packages/gyroscope-plugin/src/index.ts @@ -0,0 +1,10 @@ +import { DEFAULTS, registerButton } from '@photo-sphere-viewer/core'; +import { GyroscopeButton } from './GyroscopeButton'; +import * as events from './events'; + +DEFAULTS.lang[GyroscopeButton.id] = 'Gyroscope'; +registerButton(GyroscopeButton, 'caption:right'); + +export { GyroscopePlugin } from './GyroscopePlugin'; +export * from './model'; +export { events }; diff --git a/packages/gyroscope-plugin/src/model.ts b/packages/gyroscope-plugin/src/model.ts new file mode 100644 index 000000000..b768a326a --- /dev/null +++ b/packages/gyroscope-plugin/src/model.ts @@ -0,0 +1,17 @@ +export type GyroscopePluginConfig = { + /** + * allows to pan horizontally when the gyroscope is enabled (requires global `mousemove=true`) + * @default true + */ + touchmove?: boolean; + /** + * when true the view will ignore the current direction when enabling gyroscope control + * @default false + */ + absolutePosition?: boolean; + /** + * how the gyroscope data is used to rotate the panorama + * @default 'smooth' + */ + moveMode?: 'smooth' | 'fast'; +}; diff --git a/packages/gyroscope-plugin/tsconfig.json b/packages/gyroscope-plugin/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/gyroscope-plugin/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/gyroscope-plugin/tsup.config.js b/packages/gyroscope-plugin/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/gyroscope-plugin/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/little-planet-adapter/.typedoc/README.md b/packages/little-planet-adapter/.typedoc/README.md new file mode 100644 index 000000000..42f86f737 --- /dev/null +++ b/packages/little-planet-adapter/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/little-planet-adapter](https://www.npmjs.com/package/@photo-sphere-viewer/little-planet-adapter) + +Documentation : https://photo-sphere-viewer.js.org/guide/adapters/little-planet diff --git a/packages/little-planet-adapter/package.json b/packages/little-planet-adapter/package.json new file mode 100644 index 000000000..718cdd5a9 --- /dev/null +++ b/packages/little-planet-adapter/package.json @@ -0,0 +1,25 @@ +{ + "name": "@photo-sphere-viewer/little-planet-adapter", + "version": "0.0.0", + "description": "Photo sphere Viewer adapter for equirectangular panoramas displayed with little planet effect.", + "homepage": "https://photo-sphere-viewer.js.org/guide/adapters/little-planet", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.LittlePlanetAdapter" + }, + "typedoc": { + "displayName": "adapter: LittlePlanet", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/little-planet-adapter/src/LittlePlanetAdapter.ts b/packages/little-planet-adapter/src/LittlePlanetAdapter.ts new file mode 100644 index 000000000..08b85ad38 --- /dev/null +++ b/packages/little-planet-adapter/src/LittlePlanetAdapter.ts @@ -0,0 +1,125 @@ +import type { Position, Size, TextureData, Viewer } from '@photo-sphere-viewer/core'; +import { EquirectangularAdapter, events } from '@photo-sphere-viewer/core'; +import { BufferGeometry, Euler, MathUtils, Matrix4, Mesh, PlaneGeometry, ShaderMaterial, Texture } from 'three'; + +type EquirectangularMesh = Mesh; +type EquirectangularTexture = TextureData; + +const euler = new Euler(); + +/** + * Adapter for equirectangular panoramas displayed with little planet effect + */ +export class LittlePlanetAdapter extends EquirectangularAdapter { + static override readonly id = 'little-planet'; + static override readonly supportsDownload = false; + static override readonly supportsOverlay = false; + + private uniforms: ShaderMaterial['uniforms']; + + constructor(viewer: Viewer) { + super(viewer, undefined); + + this.viewer.state.littlePlanet = true; + + this.viewer.addEventListener(events.SizeUpdatedEvent.type, this); + this.viewer.addEventListener(events.ZoomUpdatedEvent.type, this); + this.viewer.addEventListener(events.PositionUpdatedEvent.type, this); + } + + override supportsTransition() { + return false; + } + + override supportsPreload() { + return true; + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof events.SizeUpdatedEvent) { + this.__setResolution(e.size); + } else if (e instanceof events.ZoomUpdatedEvent) { + this.__setZoom(); + } else if (e instanceof events.PositionUpdatedEvent) { + this.__setPosition(e.position); + } + } + + override createMesh(): EquirectangularMesh { + const geometry = new PlaneGeometry(20, 10).translate(0, 0, -1) as PlaneGeometry; + + // this one was copied from https://github.com/pchen66/panolens.js + const material = new ShaderMaterial({ + uniforms: { + panorama: { value: new Texture() }, + resolution: { value: 2.0 }, + transform: { value: new Matrix4() }, + zoom: { value: 10.0 }, + opacity: { value: 1.0 }, + }, + + vertexShader: ` +varying vec2 vUv; + +void main() { + vUv = uv; + gl_Position = vec4( position, 1.0 ); +}`, + + fragmentShader: ` +uniform sampler2D panorama; +uniform float resolution; +uniform mat4 transform; +uniform float zoom; +uniform float opacity; + +varying vec2 vUv; + +const float PI = 3.1415926535897932384626433832795; + +void main() { + vec2 position = -1.0 + 2.0 * vUv; + position *= vec2( zoom * resolution, zoom * 0.5 ); + + float x2y2 = position.x * position.x + position.y * position.y; + vec3 sphere_pnt = vec3( 2. * position, x2y2 - 1. ) / ( x2y2 + 1. ); + sphere_pnt = vec3( transform * vec4( sphere_pnt, 1.0 ) ); + + vec2 sampleUV = vec2( + 1.0 - (atan(sphere_pnt.y, sphere_pnt.x) / PI + 1.0) * 0.5, + (asin(sphere_pnt.z) / PI + 0.5) + ); + + gl_FragColor = texture2D( panorama, sampleUV ); + gl_FragColor.a *= opacity; +}`, + }); + + this.uniforms = material.uniforms; + + return new Mesh(geometry, material); + } + + override setTexture(mesh: EquirectangularMesh, textureData: EquirectangularTexture) { + mesh.material.uniforms.panorama.value.dispose(); + mesh.material.uniforms.panorama.value = textureData.texture; + } + + private __setResolution(size: Size) { + this.uniforms.resolution.value = size.width / size.height; + } + + private __setZoom() { + // mapping values are empirical + this.uniforms.zoom.value = Math.max(0.1, MathUtils.mapLinear(this.viewer.state.vFov, 90, 30, 50, 2)); + } + + private __setPosition(position: Position) { + euler.set(Math.PI / 2 + position.pitch, 0, -Math.PI / 2 - position.yaw, 'ZYX'); + + this.uniforms.transform.value.makeRotationFromEuler(euler); + } +} diff --git a/packages/little-planet-adapter/src/index.ts b/packages/little-planet-adapter/src/index.ts new file mode 100644 index 000000000..6718eaa65 --- /dev/null +++ b/packages/little-planet-adapter/src/index.ts @@ -0,0 +1,5 @@ +import { DEFAULTS } from '@photo-sphere-viewer/core'; + +DEFAULTS.defaultPitch = -Math.PI / 2; + +export { LittlePlanetAdapter } from './LittlePlanetAdapter'; diff --git a/packages/little-planet-adapter/tsconfig.json b/packages/little-planet-adapter/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/little-planet-adapter/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/little-planet-adapter/tsup.config.js b/packages/little-planet-adapter/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/little-planet-adapter/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/markers-plugin/.typedoc/README.md b/packages/markers-plugin/.typedoc/README.md new file mode 100644 index 000000000..35b12835a --- /dev/null +++ b/packages/markers-plugin/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/markers-plugin](https://www.npmjs.com/package/@photo-sphere-viewer/markers-plugin) + +Documentation : https://photo-sphere-viewer.js.org/plugins/markers diff --git a/packages/markers-plugin/package.json b/packages/markers-plugin/package.json new file mode 100644 index 000000000..17b64ab42 --- /dev/null +++ b/packages/markers-plugin/package.json @@ -0,0 +1,26 @@ +{ + "name": "@photo-sphere-viewer/markers-plugin", + "version": "0.0.0", + "description": "Photo sphere Viewer plugin to display various markers/hotspots on the viewer.", + "homepage": "https://photo-sphere-viewer.js.org/plugins/markers", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix && stylelint \"src/**/*.scss\" --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.MarkersPlugin", + "style": true + }, + "typedoc": { + "displayName": "plugin: Markers", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/markers-plugin/src/Marker.ts b/packages/markers-plugin/src/Marker.ts new file mode 100644 index 000000000..1372d2df9 --- /dev/null +++ b/packages/markers-plugin/src/Marker.ts @@ -0,0 +1,712 @@ +import type { Point, Position, Size, Tooltip, TooltipConfig, Viewer } from '@photo-sphere-viewer/core'; +import { CONSTANTS, PSVError, utils } from '@photo-sphere-viewer/core'; +import { Group, MathUtils, Mesh, MeshBasicMaterial, Object3D, PlaneGeometry, TextureLoader, Vector3 } from 'three'; +import { MARKER_DATA, SVG_NS } from './constants'; +import { MarkerConfig, ParsedMarkerConfig } from './model'; +import { getPolygonCenter, getPolylineCenter } from './utils'; + +export enum MarkerType { + image = 'image', + imageLayer = 'imageLayer', + html = 'html', + polygon = 'polygon', + polygonPixels = 'polygonPixels', + polyline = 'polyline', + polylinePixels = 'polylinePixels', + square = 'square', + rect = 'rect', + circle = 'circle', + ellipse = 'ellipse', + path = 'path', + + /** @deprecated */ + polygonPx = 'polygonPx', + /** @deprecated */ + polygonRad = 'polygonRad', + /** @deprecated */ + polylinePx = 'polylinePx', + /** @deprecated */ + polylineRad = 'polylineRad', +} + +const POLY_MAPPING: Partial> = { + [MarkerType.polygonPx]: MarkerType.polygonPixels, + [MarkerType.polygonRad]: MarkerType.polygon, + [MarkerType.polylinePx]: MarkerType.polylinePixels, + [MarkerType.polylineRad]: MarkerType.polyline, +}; + +export class Marker { + readonly type: MarkerType; + private readonly element: any; + + /** + * The final description of the marker. Either text content, image, url, SVG attributes, etc. + */ + definition: any; + visible = true; + + /** @internal */ + tooltip?: Tooltip; + private loader?: TextureLoader; + + config: ParsedMarkerConfig; + + get id(): string { + return this.config.id; + } + + get data(): any { + return this.config.data; + } + + get domElement(): HTMLElement | SVGElement { + return !this.is3d() ? this.element : null; + } + + get threeElement(): Object3D { + return this.is3d() ? this.element : null; + } + + /** @internal */ + readonly state = { + dynamicSize: false, + anchor: null as Point, + visible: false, + staticTooltip: false, + position: null as Position, + position2D: null as Point, + positions3D: null as Vector3[], + size: null as Size, + }; + + constructor(private readonly viewer: Viewer, config: MarkerConfig) { + if (!config.id) { + throw new PSVError('missing marker id'); + } + + this.type = Marker.getType(config); + + // create element + if (this.isNormal()) { + this.element = document.createElement('div'); + } else if (this.isPolygon()) { + this.element = document.createElementNS(SVG_NS, 'polygon'); + } else if (this.isPolyline()) { + this.element = document.createElementNS(SVG_NS, 'polyline'); + } else if (this.isSvg()) { + const svgType = this.type === MarkerType.square ? 'rect' : this.type; + this.element = document.createElementNS(SVG_NS, svgType); + } else if (this.is3d()) { + this.element = this.__createMesh(); + this.loader = new TextureLoader(); + if (this.viewer.config.withCredentials) { + this.loader.setWithCredentials(true); + } + } + + if (!this.is3d()) { + this.element.id = `psv-marker-${config.id}`; + this.element[MARKER_DATA] = this; + } + + this.update(config); + } + + /** + * @internal + */ + destroy() { + this.hideTooltip(); + + if (this.is3d()) { + delete this.threeElement.children[0].userData[MARKER_DATA]; + } else { + delete this.element[MARKER_DATA]; + } + } + + /** + * Checks if it is a 3D marker (imageLayer) + */ + is3d(): boolean { + return this.type === MarkerType.imageLayer; + } + + /** + * Checks if it is a normal marker (image or html) + */ + isNormal(): boolean { + return this.type === MarkerType.image + || this.type === MarkerType.html; + } + + /** + * Checks if it is a polygon/polyline marker + */ + isPoly(): boolean { + return this.isPolygon() + || this.isPolyline(); + } + + /** + * Checks if it is a polygon/polyline using pixel coordinates + */ + isPolyPixels(): boolean { + return this.type === MarkerType.polygonPixels + || this.type === MarkerType.polylinePixels + || this.type === MarkerType.polygonPx + || this.type === MarkerType.polylinePx; + } + + /** + * Checks if it is a polygon/polyline using radian coordinates + */ + isPolyAngles(): boolean { + return this.type === MarkerType.polygon + || this.type === MarkerType.polyline + || this.type === MarkerType.polygonRad + || this.type === MarkerType.polylineRad; + } + + /** + * Checks if it is a polygon marker + */ + isPolygon(): boolean { + return this.type === MarkerType.polygon + || this.type === MarkerType.polygonPixels + || this.type === MarkerType.polygonPx + || this.type === MarkerType.polygonRad; + } + + /** + * Checks if it is a polyline marker + */ + isPolyline(): boolean { + return this.type === MarkerType.polyline + || this.type === MarkerType.polylinePixels + || this.type === MarkerType.polylinePx + || this.type === MarkerType.polylineRad; + } + + /** + * Checks if it is an SVG marker + */ + isSvg(): boolean { + return this.type === MarkerType.square + || this.type === MarkerType.rect + || this.type === MarkerType.circle + || this.type === MarkerType.ellipse + || this.type === MarkerType.path; + } + + /** + * Computes marker scale + * @internal + */ + getScale(zoomLevel: number, position: Position): number { + if (!this.config.scale) { + return 1; + } + if (typeof this.config.scale === 'function') { + return this.config.scale(zoomLevel, position); + } + + let scale = 1; + if (Array.isArray(this.config.scale.zoom)) { + const [min, max] = this.config.scale.zoom; + scale *= min + (max - min) * CONSTANTS.EASINGS.inQuad(zoomLevel / 100); + } + if (Array.isArray(this.config.scale.yaw)) { + const [min, max] = this.config.scale.yaw; + const halfFov = MathUtils.degToRad(this.viewer.state.hFov) / 2; + const arc = Math.abs(utils.getShortestArc(this.state.position.yaw, position.pitch)); + scale *= max + (min - max) * CONSTANTS.EASINGS.outQuad(Math.max(0, (halfFov - arc) / halfFov)); + } + return scale; + } + + /** + * Returns the markers list content for the marker, it can be either : + * - the `listContent` + * - the `tooltip` + * - the `html` + * - the `id` + * @internal + */ + getListContent(): string { + if (this.config.listContent) { + return this.config.listContent; + } else if (this.config.tooltip?.content) { + return this.config.tooltip.content; + } else if (this.config.html) { + return this.config.html; + } else { + return this.id; + } + } + + /** + * Display the tooltip of this marker + * @internal + */ + showTooltip(clientX?: number, clientY?: number) { + if (this.state.visible && this.config.tooltip?.content && this.state.position2D) { + const config: TooltipConfig = { + ...this.config.tooltip, + data: this, + top: 0, + left: 0, + }; + + if (this.isPoly()) { + if (clientX || clientY) { + const viewerPos = utils.getPosition(this.viewer.container); + config.top = clientY - viewerPos.y; + config.left = clientX - viewerPos.x; + config.box = { + // separate the tooltip from the cursor + width: 20, + height: 20, + }; + } else { + config.top = this.state.position2D.y; + config.left = this.state.position2D.x; + } + } else { + config.top = this.state.position2D.y + this.state.size.height / 2; + config.left = this.state.position2D.x + this.state.size.width / 2; + config.box = { + width: this.state.size.width, + height: this.state.size.height, + }; + } + + if (this.tooltip) { + this.tooltip.move(config); + } else { + this.tooltip = this.viewer.createTooltip(config); + } + } + } + + /** + * Recompute the position of the tooltip + * @internal + */ + refreshTooltip() { + if (this.tooltip) { + this.showTooltip(); + } + } + + /** + * Hides the tooltip of this marker + * @internal + */ + hideTooltip() { + if (this.tooltip) { + this.tooltip.hide(); + this.tooltip = null; + } + } + + /** + * Updates the marker with new properties + * @throws {@link PSVError} if the configuration is invalid + * @internal + */ + update(config: MarkerConfig) { + const newType = Marker.getType(config, true); + + if (newType !== undefined && newType !== this.type) { + throw new PSVError('cannot change marker type'); + } + + if (utils.isExtendedPosition(config)) { + utils.logWarn('Use the "position" property to configure the position of a marker'); + config.position = this.viewer.dataHelper.cleanPosition(config); + } + + if ('width' in config && 'height' in config) { + utils.logWarn('Use the "size" property to configure the size of a marker'); + // @ts-ignore + config.size = { width: config['width'], height: config['height'] }; + } + + this.config = utils.deepmerge(this.config, config as any); + if (typeof this.config.tooltip === 'string') { + this.config.tooltip = { content: this.config.tooltip }; + } + if (this.config.tooltip && !this.config.tooltip.trigger) { + this.config.tooltip.trigger = 'hover'; + } + if (this.config.scale && Array.isArray(this.config.scale)) { + this.config.scale = { zoom: this.config.scale as any }; + } + + this.visible = this.config.visible !== false; + + this.state.anchor = utils.parsePoint(this.config.anchor); + + if (!this.is3d()) { + const element = this.domElement; + + // reset CSS class + if (this.isNormal()) { + element.setAttribute('class', 'psv-marker psv-marker--normal'); + } else { + element.setAttribute('class', 'psv-marker psv-marker--svg'); + } + + // add CSS classes + if (this.config.className) { + utils.addClasses(element, this.config.className); + } + + if (this.config.tooltip) { + element.classList.add('psv-marker--has-tooltip'); + } + if (this.config.content) { + element.classList.add('psv-marler--has-content'); + } + + // apply style + element.style.opacity = `${this.config.opacity ?? 1}`; + if (this.config.style) { + Object.assign(element.style, this.config.style); + } + } + + if (this.isNormal()) { + this.__updateNormal(); + } else if (this.isPoly()) { + this.__updatePoly(); + } else if (this.isSvg()) { + this.__updateSvg(); + } else if (this.is3d()) { + this.__update3d(); + } + } + + /** + * Updates a normal marker + */ + private __updateNormal() { + const element = this.domElement; + + if (!utils.isExtendedPosition(this.config.position)) { + throw new PSVError('missing marker position'); + } + if (this.config.image && !this.config.size) { + throw new PSVError('missing marker size'); + } + + if (this.config.size) { + this.state.dynamicSize = false; + this.state.size = this.config.size; + element.style.width = this.config.size.width + 'px'; + element.style.height = this.config.size.height + 'px'; + } else { + this.state.dynamicSize = true; + } + + if (this.config.image) { + this.definition = this.config.image; + element.style.backgroundImage = `url(${this.config.image})`; + } else if (this.config.html) { + this.definition = this.config.html; + element.innerHTML = this.config.html; + } + + // set anchor + element.style.transformOrigin = `${this.state.anchor.x * 100}% ${this.state.anchor.y * 100}%`; + + // convert texture coordinates to spherical coordinates + this.state.position = this.viewer.dataHelper.cleanPosition(this.config.position); + + // compute x/y/z position + this.state.positions3D = [this.viewer.dataHelper.sphericalCoordsToVector3(this.state.position)]; + } + + /** + * Updates an SVG marker + */ + private __updateSvg() { + const element = this.domElement; + + if (!utils.isExtendedPosition(this.config.position)) { + throw new PSVError('missing marker position'); + } + + this.state.dynamicSize = true; + + // set content + switch (this.type) { + case MarkerType.square: + this.definition = { + x: 0, + y: 0, + width: this.config.square, + height: this.config.square, + }; + break; + + case MarkerType.rect: + if (Array.isArray(this.config.rect)) { + this.definition = { + x: 0, + y: 0, + width: this.config.rect[0], + height: this.config.rect[1], + }; + } else { + this.definition = { + x: 0, + y: 0, + width: this.config.rect.width, + height: this.config.rect.height, + }; + } + break; + + case MarkerType.circle: + this.definition = { + cx: this.config.circle, + cy: this.config.circle, + r: this.config.circle, + }; + break; + + case MarkerType.ellipse: + if (Array.isArray(this.config.ellipse)) { + this.definition = { + cx: this.config.ellipse[0], + cy: this.config.ellipse[1], + rx: this.config.ellipse[0], + ry: this.config.ellipse[1], + }; + } else { + this.definition = { + cx: this.config.ellipse.rx, + cy: this.config.ellipse.ry, + rx: this.config.ellipse.rx, + ry: this.config.ellipse.ry, + }; + } + break; + + case MarkerType.path: + this.definition = { + d: this.config.path, + }; + break; + + // no default + } + + Object.entries(this.definition).forEach(([prop, value]) => { + element.setAttributeNS(null, prop, value as string); + }); + + // set style + if (this.config.svgStyle) { + Object.entries(this.config.svgStyle).forEach(([prop, value]) => { + element.setAttributeNS(null, utils.dasherize(prop), value); + }); + } else { + element.setAttributeNS(null, 'fill', 'rgba(0,0,0,0.5)'); + } + + // convert texture coordinates to spherical coordinates + this.state.position = this.viewer.dataHelper.cleanPosition(this.config.position); + + // compute x/y/z position + this.state.positions3D = [this.viewer.dataHelper.sphericalCoordsToVector3(this.state.position)]; + } + + /** + * Updates a polygon marker + */ + private __updatePoly() { + const element = this.domElement; + + this.state.dynamicSize = true; + + if (POLY_MAPPING[this.type]) { + utils.logWarn(`${this.type} is deprecated, use ${POLY_MAPPING[this.type]} instead`); + } + + // set style + if (this.config.svgStyle) { + Object.entries(this.config.svgStyle).forEach(([prop, value]) => { + element.setAttributeNS(null, utils.dasherize(prop), value); + }); + + if (this.isPolyline() && !this.config.svgStyle.fill) { + element.setAttributeNS(null, 'fill', 'none'); + } + } else if (this.isPolygon()) { + element.setAttributeNS(null, 'fill', 'rgba(0,0,0,0.5)'); + } else if (this.isPolyline()) { + element.setAttributeNS(null, 'fill', 'none'); + element.setAttributeNS(null, 'stroke', 'rgb(0,0,0)'); + } + + // fold arrays: [1,2,3,4] => [[1,2],[3,4]] + const actualPoly: any = this.config[this.type]; + if (!Array.isArray(actualPoly[0])) { + for (let i = 0; i < actualPoly.length; i++) { + // @ts-ignore + actualPoly.splice(i, 2, [actualPoly[i], actualPoly[i + 1]]); + } + } + + // convert texture coordinates to spherical coordinates + if (this.isPolyPixels()) { + this.definition = (actualPoly as [number, number][]).map((coord) => { + const sphericalCoords = this.viewer.dataHelper.textureCoordsToSphericalCoords({ + textureX: coord[0], + textureY: coord[1], + }); + return [sphericalCoords.yaw, sphericalCoords.pitch]; + }); + } + // clean angles + else { + this.definition = (actualPoly as [number | string, number | string][]).map((coord) => { + return [utils.parseAngle(coord[0]), utils.parseAngle(coord[1], true)]; + }); + } + + const centroid = this.isPolygon() ? getPolygonCenter(this.definition) : getPolylineCenter(this.definition); + + this.state.position = { + yaw: centroid[0], + pitch: centroid[1], + }; + + // compute x/y/z positions + this.state.positions3D = (this.definition as [number, number][]).map((coord) => { + return this.viewer.dataHelper.sphericalCoordsToVector3({ yaw: coord[0], pitch: coord[1] }); + }); + } + + /** + * Updates a 3D marker + */ + private __update3d() { + const element = this.threeElement; + + if (!utils.isExtendedPosition(this.config.position)) { + throw new PSVError('missing marker position'); + } + if (!this.config.size) { + throw new PSVError('missing marker size'); + } + + this.state.dynamicSize = false; + this.state.size = this.config.size; + + // convert texture coordinates to spherical coordinates + this.state.position = this.viewer.dataHelper.cleanPosition(this.config.position); + + // compute x/y/z position + this.state.positions3D = [this.viewer.dataHelper.sphericalCoordsToVector3(this.state.position)]; + + switch (this.type) { + case MarkerType.imageLayer: + if (this.definition !== this.config.imageLayer) { + if (this.viewer.config.requestHeaders) { + this.loader.setRequestHeader(this.viewer.config.requestHeaders(this.config.imageLayer)); + } + (element.children[0] as Mesh).material.map = this.loader.load( + this.config.imageLayer, + (texture) => { + texture.anisotropy = 4; + this.viewer.needsUpdate(); + } + ); + this.definition = this.config.imageLayer; + } + + (element.children[0] as Mesh).position.set(this.state.anchor.x - 0.5, this.state.anchor.y - 0.5, 0); + + (element.children[0] as Mesh).material.opacity = this.config.opacity ?? 1; + + element.position.copy(this.state.positions3D[0]); + + switch (this.config.orientation) { + case 'horizontal': + element.lookAt(0, element.position.y, 0); + element.rotateX(this.state.position.pitch < 0 ? -Math.PI / 2 : Math.PI / 2); + break; + case 'vertical-left': + element.lookAt(0, 0, 0); + element.rotateY(-Math.PI * 0.4); + break; + case 'vertical-right': + element.lookAt(0, 0, 0); + element.rotateY(Math.PI * 0.4); + break; + default: + element.lookAt(0, 0, 0); + break; + } + + // 100 is magic number that gives a coherent size at default zoom level + element.scale.set(this.config.size.width / 100, this.config.size.height / 100, 1); + break; + + // no default + } + } + + private __createMesh() { + const material = new MeshBasicMaterial({ + transparent: true, + opacity: 1, + depthTest: false, + }); + const geometry = new PlaneGeometry(1, 1); + const mesh = new Mesh(geometry, material); + mesh.userData = { [MARKER_DATA]: this }; + const element = new Group().add(mesh); + + // overwrite the visible property to be tied to the Marker instance + // and do it without context bleed + Object.defineProperty(element, 'visible', { + enumerable: true, + get: function (this: Object3D) { + return (this.children[0].userData[MARKER_DATA] as Marker).visible; + }, + set: function (this: Object3D, visible: boolean) { + (this.children[0].userData[MARKER_DATA] as Marker).visible = visible; + }, + }); + + return element; + } + + /** + * Determines the type of a marker by the available properties + * @throws {@link PSVError} when the marker's type cannot be found + */ + static getType(config: MarkerConfig, allowNone = false): MarkerType { + const found: MarkerType[] = []; + + Object.keys(MarkerType).forEach((type) => { + if ((config as any)[type]) { + found.push(type as MarkerType); + } + }); + + if (found.length === 0 && !allowNone) { + throw new PSVError(`missing marker content, either ${Object.keys(MarkerType).join(', ')}`); + } else if (found.length > 1) { + throw new PSVError(`multiple marker content, either ${Object.keys(MarkerType).join(', ')}`); + } + + return found[0]; + } +} diff --git a/packages/markers-plugin/src/MarkersButton.ts b/packages/markers-plugin/src/MarkersButton.ts new file mode 100644 index 000000000..e8daa75e2 --- /dev/null +++ b/packages/markers-plugin/src/MarkersButton.ts @@ -0,0 +1,55 @@ +import type { Navbar } from '@photo-sphere-viewer/core'; +import { AbstractButton } from '@photo-sphere-viewer/core'; +import { HideMarkersEvent, ShowMarkersEvent } from './events'; +import type { MarkersPlugin } from './MarkersPlugin'; +import pin from './icons/pin.svg'; + +export class MarkersButton extends AbstractButton { + static override readonly id = 'markers'; + + private readonly plugin: MarkersPlugin; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-markers-button', + icon: pin, + hoverScale: true, + collapsable: true, + tabbable: true, + }); + + this.plugin = this.viewer.getPlugin('markers'); + + if (this.plugin) { + this.plugin.addEventListener(ShowMarkersEvent.type, this); + this.plugin.addEventListener(HideMarkersEvent.type, this); + + this.toggleActive(true); + } + } + + override destroy() { + if (this.plugin) { + this.plugin.removeEventListener(ShowMarkersEvent.type, this); + this.plugin.removeEventListener(HideMarkersEvent.type, this); + } + + super.destroy(); + } + + override isSupported() { + return !!this.plugin; + } + + handleEvent(e: Event) { + if (e instanceof ShowMarkersEvent) { + this.toggleActive(true); + } else if (e instanceof HideMarkersEvent) { + this.toggleActive(false); + } + } + + onClick() { + this.plugin.toggleAllMarkers(); + } +} diff --git a/packages/markers-plugin/src/MarkersListButton.ts b/packages/markers-plugin/src/MarkersListButton.ts new file mode 100644 index 000000000..a3b45648f --- /dev/null +++ b/packages/markers-plugin/src/MarkersListButton.ts @@ -0,0 +1,51 @@ +import type { Navbar } from '@photo-sphere-viewer/core'; +import { AbstractButton, events } from '@photo-sphere-viewer/core'; +import { ID_PANEL_MARKERS_LIST } from './constants'; +import type { MarkersPlugin } from './MarkersPlugin'; +import pinList from './icons/pin-list.svg'; + +export class MarkersListButton extends AbstractButton { + static override readonly id = 'markersList'; + + private readonly plugin: MarkersPlugin; + + constructor(navbar: Navbar) { + super(navbar, { + className: ' psv-markers-list-button', + icon: pinList, + hoverScale: true, + collapsable: true, + tabbable: true, + }); + + this.plugin = this.viewer.getPlugin('markers'); + + if (this.plugin) { + this.viewer.addEventListener(events.ShowPanelEvent.type, this); + this.viewer.addEventListener(events.HidePanelEvent.type, this); + } + } + + override destroy() { + this.viewer.removeEventListener(events.ShowPanelEvent.type, this); + this.viewer.removeEventListener(events.HidePanelEvent.type, this); + + super.destroy(); + } + + override isSupported() { + return !!this.plugin; + } + + handleEvent(e: Event) { + if (e instanceof events.ShowPanelEvent) { + this.toggleActive(e.panelId === ID_PANEL_MARKERS_LIST); + } else if (e instanceof events.HidePanelEvent) { + this.toggleActive(false); + } + } + + onClick() { + this.plugin.toggleMarkersList(); + } +} diff --git a/packages/markers-plugin/src/MarkersPlugin.ts b/packages/markers-plugin/src/MarkersPlugin.ts new file mode 100644 index 000000000..23d0af719 --- /dev/null +++ b/packages/markers-plugin/src/MarkersPlugin.ts @@ -0,0 +1,921 @@ +import type { Point, Tooltip, Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin, CONSTANTS, events, PSVError, utils } from '@photo-sphere-viewer/core'; +import { Vector3 } from 'three'; +import { ID_PANEL_MARKER, ID_PANEL_MARKERS_LIST, MARKERS_LIST_TEMPLATE, MARKER_DATA, SVG_NS } from './constants'; +import { + EnterMarkerEvent, + GotoMarkerDoneEvent, + HideMarkersEvent, + LeaveMarkerEvent, + MarkersPluginEvents, + MarkerVisibilityEvent, + RenderMarkersListEvent, + SelectMarkerEvent, + SelectMarkerListEvent, + SetMarkersEvent, + ShowMarkersEvent, + UnselectMarkerEvent, +} from './events'; +import { Marker } from './Marker'; +import { MarkersButton } from './MarkersButton'; +import { MarkersListButton } from './MarkersListButton'; +import { MarkerConfig, MarkersPluginConfig } from './model'; + +const getConfig = utils.getConfigParser({ + clickEventOnMarker: false, + gotoMarkerSpeed: '8rpm', + markers: null, +}); + +/** + * Displays various markers on the viewer + */ +export class MarkersPlugin extends AbstractPlugin { + static override readonly id = 'markers'; + + readonly config: MarkersPluginConfig; + + private readonly markers: Record = {}; + + private readonly state = { + visible: true, + showAllTooltips: false, + currentMarker: null as Marker, + hoveringMarker: null as Marker, + }; + + private readonly container: HTMLElement; + private readonly svgContainer: SVGElement; + + constructor(viewer: Viewer, config: MarkersPluginConfig) { + super(viewer); + + this.config = getConfig(config); + + this.container = document.createElement('div'); + this.container.className = 'psv-markers'; + this.container.style.cursor = this.viewer.config.mousemove ? 'move' : 'default'; + + this.svgContainer = document.createElementNS(SVG_NS, 'svg'); + this.svgContainer.setAttribute('class', 'psv-markers-svg-container'); + this.container.appendChild(this.svgContainer); + + // Markers events via delegation + this.container.addEventListener('mouseenter', this, true); + this.container.addEventListener('mouseleave', this, true); + this.container.addEventListener('mousemove', this, true); + this.container.addEventListener('contextmenu', this); + } + + /** + * @internal + */ + override init() { + super.init(); + + this.viewer.container.appendChild(this.container); + + // Viewer events + this.viewer.addEventListener(events.ClickEvent.type, this); + this.viewer.addEventListener(events.DoubleClickEvent.type, this); + this.viewer.addEventListener(events.RenderEvent.type, this); + this.viewer.addEventListener(events.ConfigChangedEvent.type, this); + this.viewer.addEventListener(events.ObjectEnterEvent.type, this); + this.viewer.addEventListener(events.ObjectHoverEvent.type, this); + this.viewer.addEventListener(events.ObjectLeaveEvent.type, this); + this.viewer.addEventListener(events.ReadyEvent.type, this, { once: true }); + } + + /** + * @internal + */ + override destroy() { + this.clearMarkers(false); + + this.viewer.unobserveObjects(MARKER_DATA); + + this.viewer.removeEventListener(events.ClickEvent.type, this); + this.viewer.removeEventListener(events.DoubleClickEvent.type, this); + this.viewer.removeEventListener(events.RenderEvent.type, this); + this.viewer.removeEventListener(events.ConfigChangedEvent.type, this); + this.viewer.removeEventListener(events.ObjectEnterEvent.type, this); + this.viewer.removeEventListener(events.ObjectHoverEvent.type, this); + this.viewer.removeEventListener(events.ObjectLeaveEvent.type, this); + this.viewer.removeEventListener(events.ReadyEvent.type, this); + + this.viewer.container.removeChild(this.container); + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + switch (e.type) { + case events.ReadyEvent.type: + if (this.config.markers) { + this.setMarkers(this.config.markers); + delete this.config.markers; + } + break; + + case events.RenderEvent.type: + this.renderMarkers(); + break; + + case events.ClickEvent.type: + this.__onClick(e as events.ClickEvent, false); + break; + + case events.DoubleClickEvent.type: + this.__onClick(e as events.DoubleClickEvent, true); + break; + + case events.ConfigChangedEvent.type: + this.container.style.cursor = this.viewer.config.mousemove ? 'move' : 'default'; + break; + + case events.ObjectEnterEvent.type: + case events.ObjectLeaveEvent.type: + case events.ObjectHoverEvent.type: + if ((e as events.ObjectEvent).userDataKey === MARKER_DATA) { + const event = (e as events.ObjectEvent).originalEvent; + const marker = (e as events.ObjectEvent).object.userData[MARKER_DATA]; + switch (e.type) { + case events.ObjectEnterEvent.type: + this.__onMouseEnter(event, marker); + break; + case events.ObjectLeaveEvent.type: + this.__onMouseLeave(event, marker); + break; + case events.ObjectHoverEvent.type: + this.__onMouseMove(event, marker); + break; + } + } + break; + + case 'mouseenter': + this.__onMouseEnter(e as MouseEvent, this.__getTargetMarker(e.target as HTMLElement)); + break; + + case 'mouseleave': + this.__onMouseLeave(e as MouseEvent, this.__getTargetMarker(e.target as HTMLElement)); + break; + + case 'mousemove': + this.__onMouseMove(e as MouseEvent, this.__getTargetMarker(e.target as HTMLElement)); + break; + + case 'contextmenu': + e.preventDefault(); + break; + } + } + + /** + * Toggles all markers + */ + toggleAllMarkers() { + if (this.state.visible) { + this.hideAllMarkers(); + } else { + this.showAllMarkers(); + } + } + + /** + * Shows all markers + */ + showAllMarkers() { + this.state.visible = true; + + this.renderMarkers(); + + this.dispatchEvent(new ShowMarkersEvent()); + } + + /** + * Hides all markers + */ + hideAllMarkers() { + this.state.visible = false; + + this.renderMarkers(); + + this.dispatchEvent(new HideMarkersEvent()); + } + + /** + * Toggles the visibility of all tooltips + */ + toggleAllTooltips() { + if (this.state.showAllTooltips) { + this.hideAllTooltips(); + } else { + this.showAllTooltips(); + } + } + + /** + * Displays all tooltips + */ + showAllTooltips() { + this.state.showAllTooltips = true; + Object.values(this.markers).forEach((marker) => { + marker.state.staticTooltip = true; + marker.showTooltip(); + }); + } + + /** + * Hides all tooltips + */ + hideAllTooltips() { + this.state.showAllTooltips = false; + Object.values(this.markers).forEach((marker) => { + marker.state.staticTooltip = false; + marker.hideTooltip(); + }); + } + + /** + * Returns the total number of markers + */ + getNbMarkers(): number { + return Object.keys(this.markers).length; + } + + /** + * Returns all the markers + */ + getMarkers(): Marker[] { + return Object.values(this.markers); + } + + /** + * Adds a new marker to viewer + * @throws {@link PSVError} when the marker's id is missing or already exists + */ + addMarker(config: MarkerConfig, render = true) { + if (this.markers[config.id]) { + throw new PSVError(`marker "${config.id}" already exists`); + } + + const marker = new Marker(this.viewer, config); + + if (marker.isNormal()) { + this.container.appendChild(marker.domElement); + } else if (marker.isPoly() || marker.isSvg()) { + this.svgContainer.appendChild(marker.domElement); + } else if (marker.is3d()) { + this.viewer.renderer.addObject(marker.threeElement); + } else { + throw new PSVError('invalid state'); + } + + this.markers[marker.id] = marker; + + if (render) { + this.__afterChangerMarkers(); + } + } + + /** + * Returns the internal marker object for a marker id + * @throws {@link PSVError} when the marker cannot be found + */ + getMarker(markerId: string | MarkerConfig): Marker { + const id = typeof markerId === 'object' ? markerId.id : markerId; + + if (!this.markers[id]) { + throw new PSVError(`cannot find marker "${id}"`); + } + + return this.markers[id]; + } + + /** + * Returns the last marker selected by the user + */ + getCurrentMarker(): Marker { + return this.state.currentMarker; + } + + /** + * Updates the existing marker with the same id + * @description Every property can be changed but you can't change its type (Eg: `image` to `html`) + */ + updateMarker(config: MarkerConfig, render = true) { + const marker = this.getMarker(config.id); + + marker.update(config); + + if (render) { + this.__afterChangerMarkers(); + } + } + + /** + * Removes a marker from the viewer + */ + removeMarker(markerId: string | MarkerConfig, render = true) { + const marker = this.getMarker(markerId); + + if (marker.isNormal()) { + this.container.removeChild(marker.domElement); + } else if (marker.isPoly() || marker.isSvg()) { + this.svgContainer.removeChild(marker.domElement); + } else if (marker.is3d()) { + this.viewer.renderer.removeObject(marker.threeElement); + } + + if (this.state.hoveringMarker === marker) { + this.state.hoveringMarker = null; + } + + if (this.state.currentMarker === marker) { + this.state.currentMarker = null; + } + + marker.destroy(); + delete this.markers[marker.id]; + + if (render) { + this.__afterChangerMarkers(); + } + } + + /** + * Removes multiple markers + */ + removeMarkers(markerIds: string[], render = true) { + markerIds.forEach((markerId) => this.removeMarker(markerId, false)); + + if (render) { + this.__afterChangerMarkers(); + } + } + + /** + * Replaces all markers + */ + setMarkers(markers: MarkerConfig[], render = true) { + this.clearMarkers(false); + + markers?.forEach((marker) => { + this.addMarker(marker, false); + }); + + if (render) { + this.__afterChangerMarkers(); + } + } + + /** + * Removes all markers + */ + clearMarkers(render = true) { + Object.keys(this.markers).forEach((markerId) => { + this.removeMarker(markerId, false); + }); + + if (render) { + this.__afterChangerMarkers(); + } + } + + /** + * Rotate the view to face the marker + */ + gotoMarker(markerId: string | MarkerConfig, speed: string | number = this.config.gotoMarkerSpeed): Promise { + const marker = this.getMarker(markerId); + + if (!speed) { + this.viewer.rotate(marker.state.position); + if (!utils.isNil(marker.config.zoomLvl)) { + this.viewer.zoom(marker.config.zoomLvl); + } + this.dispatchEvent(new GotoMarkerDoneEvent(marker)); + return Promise.resolve(); + } else { + return this.viewer + .animate({ + ...marker.state.position, + zoom: marker.config.zoomLvl, + speed: speed, + }) + .then(() => { + this.dispatchEvent(new GotoMarkerDoneEvent(marker)); + }); + } + } + + /** + * Hides a marker + */ + hideMarker(markerId: string | MarkerConfig) { + this.toggleMarker(markerId, false); + } + + /** + * Shows a marker + */ + showMarker(markerId: string | MarkerConfig) { + this.toggleMarker(markerId, true); + } + + /** + * Forces the display of the tooltip of a marker + */ + showMarkerTooltip(markerId: string | MarkerConfig) { + const marker = this.getMarker(markerId); + marker.state.staticTooltip = true; + marker.showTooltip(); + } + + /** + * Hides the tooltip of a marker + */ + hideMarkerTooltip(markerId: string | MarkerConfig) { + const marker = this.getMarker(markerId); + marker.state.staticTooltip = false; + marker.hideTooltip(); + } + + /** + * Toggles a marker visibility + */ + toggleMarker(markerId: string | MarkerConfig, visible?: boolean) { + const marker = this.getMarker(markerId); + marker.visible = visible === null ? !marker.visible : visible; + this.viewer.needsUpdate(); + } + + /** + * Opens the panel with the content of the marker + */ + showMarkerPanel(markerId: string | MarkerConfig) { + const marker = this.getMarker(markerId); + + if (marker?.config?.content) { + this.viewer.panel.show({ + id: ID_PANEL_MARKER, + content: marker.config.content, + }); + } else { + this.hideMarkerPanel(); + } + } + + /** + * Closes the panel if currently showing the content of a marker + */ + hideMarkerPanel() { + this.viewer.panel.hide(ID_PANEL_MARKER); + } + + /** + * Toggles the visibility of the list of markers + */ + toggleMarkersList() { + if (this.viewer.panel.isVisible(ID_PANEL_MARKERS_LIST)) { + this.hideMarkersList(); + } else { + this.showMarkersList(); + } + } + + /** + * Opens side panel with the list of markers + */ + showMarkersList() { + let markers: Marker[] = []; + Object.values(this.markers).forEach((marker) => { + if (marker.visible && !marker.config.hideList) { + markers.push(marker); + } + }); + + const e = new RenderMarkersListEvent(markers); + this.dispatchEvent(e); + markers = e.markers; + + this.viewer.panel.show({ + id: ID_PANEL_MARKERS_LIST, + content: MARKERS_LIST_TEMPLATE(markers, this.viewer.config.lang[MarkersButton.id]), + noMargin: true, + clickHandler: (target) => { + const li = utils.getClosest(target, 'li'); + const markerId = li ? li.dataset[MARKER_DATA] : undefined; + + if (markerId) { + const marker = this.getMarker(markerId); + + this.dispatchEvent(new SelectMarkerListEvent(marker)); + + this.gotoMarker(marker.id); + this.hideMarkersList(); + } + }, + }); + } + + /** + * Closes side panel if it contains the list of markers + */ + hideMarkersList() { + this.viewer.panel.hide(ID_PANEL_MARKERS_LIST); + } + + /** + * Updates the visibility and the position of all markers + */ + renderMarkers() { + const zoomLevel = this.viewer.getZoomLevel(); + const viewerPosition = this.viewer.getPosition(); + + Object.values(this.markers).forEach((marker) => { + let isVisible = this.state.visible && marker.visible; + let visibilityChanged = false; + let position: Point = null; + + if (isVisible && marker.is3d()) { + position = this.__getMarkerPosition(marker); + isVisible = this.__isMarkerVisible(marker, position); + } else if (isVisible && marker.isPoly()) { + const positions = this.__getPolyPositions(marker); + isVisible = positions.length > (marker.isPolygon() ? 2 : 1); + + if (isVisible) { + position = this.__getMarkerPosition(marker); + + const points = positions.map((pos) => pos.x - position.x + ',' + (pos.y - position.y)).join(' '); + + marker.domElement.setAttributeNS(null, 'points', points); + marker.domElement.setAttributeNS(null, 'transform', `translate(${position.x} ${position.y})`); + } + } else if (isVisible) { + if (marker.state.dynamicSize) { + this.__updateMarkerSize(marker); + } + + position = this.__getMarkerPosition(marker); + isVisible = this.__isMarkerVisible(marker, position); + + if (isVisible) { + const scale = marker.getScale(zoomLevel, viewerPosition); + + if (marker.isSvg()) { + // simulate transform-origin relative to SVG element + const x = position.x + marker.state.size.width * marker.state.anchor.x * (1 - scale); + const y = position.y + marker.state.size.height * marker.state.anchor.y * (1 - scale); + marker.domElement.setAttributeNS( + null, + 'transform', + `translate(${x}, ${y}) scale(${scale}, ${scale})` + ); + } else { + marker.domElement.style.transform = `translate3D(${position.x}px, ${position.y}px, 0px) scale(${scale}, ${scale})`; + } + } + } + + visibilityChanged = marker.state.visible !== isVisible; + marker.state.visible = isVisible; + marker.state.position2D = isVisible ? position : null; + + if (!marker.is3d()) { + utils.toggleClass(marker.domElement, 'psv-marker--visible', isVisible); + } + + if (!isVisible) { + marker.hideTooltip(); + } else if (marker.state.staticTooltip) { + marker.showTooltip(); + } else if ( + marker.config.tooltip?.trigger === 'click' + || (marker === this.state.hoveringMarker && !marker.isPoly()) + ) { + marker.refreshTooltip(); + } else if (marker !== this.state.hoveringMarker) { + marker.hideTooltip(); + } + + if (visibilityChanged) { + this.dispatchEvent(new MarkerVisibilityEvent(marker, isVisible)); + } + }); + } + + /** + * Determines if a point marker is visible
+ * It tests if the point is in the general direction of the camera, then check if it's in the viewport + */ + private __isMarkerVisible(marker: Marker, position: Point): boolean { + return marker.state.positions3D[0].dot(this.viewer.state.direction) > 0 + && position.x + marker.state.size.width >= 0 + && position.x - marker.state.size.width <= this.viewer.state.size.width + && position.y + marker.state.size.height >= 0 + && position.y - marker.state.size.height <= this.viewer.state.size.height; + } + + /** + * Computes the real size of a marker + * @description This is done by removing all it's transformations (if any) and making it visible + * before querying its bounding rect + */ + private __updateMarkerSize(marker: Marker) { + const element = marker.domElement; + + element.classList.add('psv-marker--transparent'); + + let transform; + if (marker.isSvg()) { + transform = element.getAttributeNS(null, 'transform'); + element.removeAttributeNS(null, 'transform'); + } else { + transform = element.style.transform; + element.style.transform = ''; + } + + const rect = element.getBoundingClientRect(); + marker.state.size = { + width: rect.width, + height: rect.height, + }; + + element.classList.remove('psv-marker--transparent'); + + if (transform) { + if (marker.isSvg()) { + element.setAttributeNS(null, 'transform', transform); + } else { + element.style.transform = transform; + } + } + + // the size is no longer dynamic once known + marker.state.dynamicSize = false; + } + + /** + * Computes viewer coordinates of a marker + */ + private __getMarkerPosition(marker: Marker): Point { + if (marker.isPoly()) { + return this.viewer.dataHelper.sphericalCoordsToViewerCoords(marker.state.position); + } else { + const position = this.viewer.dataHelper.vector3ToViewerCoords(marker.state.positions3D[0]); + + position.x -= marker.state.size.width * marker.state.anchor.x; + position.y -= marker.state.size.height * marker.state.anchor.y; + + return position; + } + } + + /** + * Computes viewer coordinates of each point of a polygon/polyline
+ * It handles points behind the camera by creating intermediary points suitable for the projector + */ + private __getPolyPositions(marker: Marker): Point[] { + const nbVectors = marker.state.positions3D.length; + + // compute if each vector is visible + const positions3D = marker.state.positions3D.map((vector) => { + return { + vector: vector, + visible: vector.dot(this.viewer.state.direction) > 0, + }; + }); + + // get pairs of visible/invisible vectors for each invisible vector connected to a visible vector + const toBeComputed: { visible: Vector3; invisible: Vector3; index: number }[] = []; + positions3D.forEach((pos, i) => { + if (!pos.visible) { + const neighbours = [ + i === 0 ? positions3D[nbVectors - 1] : positions3D[i - 1], + i === nbVectors - 1 ? positions3D[0] : positions3D[i + 1], + ]; + + neighbours.forEach((neighbour) => { + if (neighbour.visible) { + toBeComputed.push({ + visible: neighbour.vector, + invisible: pos.vector, + index: i, + }); + } + }); + } + }); + + // compute intermediary vector for each pair (the loop is reversed for splice to insert at the right place) + toBeComputed.reverse().forEach((pair) => { + positions3D.splice(pair.index, 0, { + vector: this.__getPolyIntermediaryPoint(pair.visible, pair.invisible), + visible: true, + }); + }); + + // translate vectors to screen pos + return positions3D + .filter((pos) => pos.visible) + .map((pos) => this.viewer.dataHelper.vector3ToViewerCoords(pos.vector)); + } + + /** + * Given one point in the same direction of the camera and one point behind the camera, + * computes an intermediary point on the great circle delimiting the half sphere visible by the camera. + * The point is shifted by .01 rad because the projector cannot handle points exactly on this circle. + * @todo : does not work with fisheye view (must not use the great circle) + * @link http://math.stackexchange.com/a/1730410/327208 + */ + private __getPolyIntermediaryPoint(P1: Vector3, P2: Vector3): Vector3 { + const C = this.viewer.state.direction.clone().normalize(); + const N = new Vector3().crossVectors(P1, P2).normalize(); + const V = new Vector3().crossVectors(N, P1).normalize(); + const X = P1.clone().multiplyScalar(-C.dot(V)); + const Y = V.clone().multiplyScalar(C.dot(P1)); + const H = new Vector3().addVectors(X, Y).normalize(); + const a = new Vector3().crossVectors(H, C); + return H.applyAxisAngle(a, 0.01).multiplyScalar(CONSTANTS.SPHERE_RADIUS); + } + + /** + * Returns the marker associated to an event target + */ + private __getTargetMarker(target: HTMLElement, closest = false): Marker { + const target2 = closest ? utils.getClosest(target, '.psv-marker') : target; + return target2 ? (target2 as any)[MARKER_DATA] : undefined; + } + + /** + * Checks if an event target is in the tooltip + */ + private __targetOnTooltip(target: HTMLElement, tooltip: Tooltip): boolean { + return target && tooltip ? utils.hasParent(target, tooltip.container) : false; + } + + /** + * Handles mouse enter events, show the tooltip for non polygon markers + */ + private __onMouseEnter(e: MouseEvent, marker: Marker) { + if (marker && !marker.isPoly()) { + this.state.hoveringMarker = marker; + + this.dispatchEvent(new EnterMarkerEvent(marker)); + + if (!marker.state.staticTooltip && marker.config.tooltip?.trigger === 'hover') { + marker.showTooltip(e.clientX, e.clientY); + } + } + } + + /** + * Handles mouse leave events, hide the tooltip + */ + private __onMouseLeave(e: MouseEvent, marker: Marker) { + // do not hide if we enter the tooltip itself while hovering a polygon + if (marker && !(marker.isPoly() && this.__targetOnTooltip(e.relatedTarget as HTMLElement, marker.tooltip))) { + this.dispatchEvent(new LeaveMarkerEvent(marker)); + + this.state.hoveringMarker = null; + + if (!marker.state.staticTooltip && marker.config.tooltip?.trigger === 'hover') { + marker.hideTooltip(); + } + } + } + + /** + * Handles mouse move events, refreshUi the tooltip for polygon markers + */ + private __onMouseMove(e: MouseEvent, targetMarker?: Marker) { + let marker; + + if (targetMarker?.isPoly()) { + marker = targetMarker; + } + // do not hide if we enter the tooltip itself while hovering a polygon + else if ( + this.state.hoveringMarker + && this.__targetOnTooltip(e.target as HTMLElement, this.state.hoveringMarker.tooltip) + ) { + marker = this.state.hoveringMarker; + } + + if (marker) { + if (!this.state.hoveringMarker) { + this.dispatchEvent(new EnterMarkerEvent(marker)); + + this.state.hoveringMarker = marker; + } + + if (!marker.state.staticTooltip) { + marker.showTooltip(e.clientX, e.clientY); + } + } else if (this.state.hoveringMarker?.isPoly()) { + this.dispatchEvent(new LeaveMarkerEvent(this.state.hoveringMarker)); + + if (!this.state.hoveringMarker.state.staticTooltip) { + this.state.hoveringMarker.hideTooltip(); + } + + this.state.hoveringMarker = null; + } + } + + /** + * Handles mouse click events, select the marker and open the panel if necessary + */ + private __onClick(e: events.ClickEvent | events.DoubleClickEvent, dblclick: boolean) { + let marker = e.data.objects.find((o) => o.userData[MARKER_DATA])?.userData[MARKER_DATA]; + + if (!marker) { + marker = this.__getTargetMarker(e.data.target, true); + } + + if (this.state.currentMarker && this.state.currentMarker !== marker) { + this.dispatchEvent(new UnselectMarkerEvent(this.state.currentMarker)); + + this.viewer.panel.hide(ID_PANEL_MARKER); + + if (!this.state.showAllTooltips && this.state.currentMarker.config.tooltip?.trigger === 'click') { + this.hideMarkerTooltip(this.state.currentMarker.id); + } + + this.state.currentMarker = null; + } + + if (marker) { + this.state.currentMarker = marker; + + this.dispatchEvent(new SelectMarkerEvent(marker, dblclick, e.data.rightclick)); + + if (this.config.clickEventOnMarker) { + // add the marker to event data + e.data.marker = marker; + } else { + e.stopImmediatePropagation(); + } + + // the marker could have been deleted in an event handler + if (this.markers[marker.id]) { + if (marker.config.tooltip?.trigger === 'click') { + if (marker.tooltip) { + this.hideMarkerTooltip(marker); + } else { + this.showMarkerTooltip(marker); + } + } else { + this.showMarkerPanel(marker.id); + } + } + } + } + + private __afterChangerMarkers() { + this.__refreshUi(); + this.__checkObjectsObserver(); + this.viewer.needsUpdate(); + this.dispatchEvent(new SetMarkersEvent(this.getMarkers())); + } + + /** + * Updates the visiblity of the panel and the buttons + */ + private __refreshUi() { + const nbMarkers = Object.values(this.markers).filter((m) => !m.config.hideList).length; + + if (nbMarkers === 0) { + if (this.viewer.panel.isVisible(ID_PANEL_MARKERS_LIST) || this.viewer.panel.isVisible(ID_PANEL_MARKER)) { + this.viewer.panel.hide(); + } + } else { + if (this.viewer.panel.isVisible(ID_PANEL_MARKERS_LIST)) { + this.showMarkersList(); + } else if (this.viewer.panel.isVisible(ID_PANEL_MARKER)) { + this.state.currentMarker ? this.showMarkerPanel(this.state.currentMarker.id) : this.viewer.panel.hide(); + } + } + + this.viewer.navbar.getButton(MarkersButton.id, false)?.toggle(nbMarkers > 0); + this.viewer.navbar.getButton(MarkersListButton.id, false)?.toggle(nbMarkers > 0); + } + + /** + * Adds or remove the objects observer if there are 3D markers + */ + private __checkObjectsObserver() { + const has3d = Object.values(this.markers).some((marker) => marker.is3d()); + + if (has3d) { + this.viewer.observeObjects(MARKER_DATA); + } else { + this.viewer.unobserveObjects(MARKER_DATA); + } + } +} diff --git a/packages/markers-plugin/src/constants.ts b/packages/markers-plugin/src/constants.ts new file mode 100644 index 000000000..939f3663b --- /dev/null +++ b/packages/markers-plugin/src/constants.ts @@ -0,0 +1,51 @@ +import { utils } from '@photo-sphere-viewer/core'; +import type { Marker } from './Marker'; +import icon from './icons/pin-list.svg'; + +/** + * Namespace for SVG creation + * @internal + */ +export const SVG_NS = 'http://www.w3.org/2000/svg'; + +/** + * Property name added to marker elements + * @internal + */ +export const MARKER_DATA = 'psvMarker'; + +/** + * Property name added to marker elements (dash-case) + * @internal + */ +export const MARKER_DATA_KEY = utils.dasherize(MARKER_DATA); + +/** + * Panel identifier for marker content + * @internal + */ +export const ID_PANEL_MARKER = 'marker'; + +/** + * Panel identifier for markers list + * @internal + */ +export const ID_PANEL_MARKERS_LIST = 'markersList'; + +/** + * Markers list template + * @internal + */ +export const MARKERS_LIST_TEMPLATE = (markers: Marker[], title: string) => ` +
+

${icon} ${title}

+
    + ${markers.map((marker) => ` +
  • + ${marker.type === 'image' ? `` : ''} + ${marker.getListContent()} +
  • + `).join('')} +
+
+`; diff --git a/packages/markers-plugin/src/events.ts b/packages/markers-plugin/src/events.ts new file mode 100644 index 000000000..dfd012ae4 --- /dev/null +++ b/packages/markers-plugin/src/events.ts @@ -0,0 +1,148 @@ +import { TypedEvent } from '@photo-sphere-viewer/core'; +import type { Marker } from './Marker'; +import type { MarkersPlugin } from './MarkersPlugin'; + +/** + * Base class for events dispatched by {@link MarkersPlugin} + */ +export abstract class MarkersPluginEvent extends TypedEvent {} + +/** + * @event Triggered when the visibility of a marker changes + */ +export class MarkerVisibilityEvent extends MarkersPluginEvent { + static override readonly type = 'marker-visibility'; + + constructor(public readonly marker: Marker, public readonly visible: boolean) { + super(MarkerVisibilityEvent.type); + } +} + +/** + * @event Triggered when the animation to a marker is done + */ +export class GotoMarkerDoneEvent extends MarkersPluginEvent { + static override readonly type = 'goto-marker-done'; + + constructor(public readonly marker: Marker) { + super(GotoMarkerDoneEvent.type); + } +} + +/** + * @event Triggered when the user puts the cursor away from a marker + */ +export class LeaveMarkerEvent extends MarkersPluginEvent { + static override readonly type = 'leave-marker'; + + constructor(public readonly marker: Marker) { + super(LeaveMarkerEvent.type); + } +} + +/** + * @event Triggered when the user puts the cursor hover a marker + */ +export class EnterMarkerEvent extends MarkersPluginEvent { + static override readonly type = 'enter-marker'; + + constructor(public readonly marker: Marker) { + super(EnterMarkerEvent.type); + } +} + +/** + * @event Triggered when the user clicks on a marker + */ +export class SelectMarkerEvent extends MarkersPluginEvent { + static override readonly type = 'select-marker'; + + constructor( + public readonly marker: Marker, + public readonly doubleClick: boolean, + public readonly rightClick: boolean + ) { + super(SelectMarkerEvent.type); + } +} + +/** + * @event Triggered when a marker is selected from the side panel + */ +export class SelectMarkerListEvent extends MarkersPluginEvent { + static override readonly type = 'select-marker-list'; + + constructor(public readonly marker: Marker) { + super(SelectMarkerListEvent.type); + } +} + +/** + * @event Triggered when a marker was selected and the user clicks elsewhere + */ +export class UnselectMarkerEvent extends MarkersPluginEvent { + static override readonly type = 'unselect-marker'; + + constructor(public readonly marker: Marker) { + super(UnselectMarkerEvent.type); + } +} + +/** + * @event Triggered when the markers are hidden + */ +export class HideMarkersEvent extends MarkersPluginEvent { + static override readonly type = 'hide-markers'; + + constructor() { + super(HideMarkersEvent.type); + } +} + +/** + * @event Triggered when the markers change + */ +export class SetMarkersEvent extends MarkersPluginEvent { + static override readonly type = 'set-markers'; + + constructor(public readonly markers: Marker[]) { + super(SetMarkersEvent.type); + } +} + +/** + * @event Triggered when the markers are shown + */ +export class ShowMarkersEvent extends MarkersPluginEvent { + static override readonly type = 'show-markers'; + + constructor() { + super(ShowMarkersEvent.type); + } +} + +/** + * @event Used to alter the list of markers displayed in the side-panel + */ +export class RenderMarkersListEvent extends MarkersPluginEvent { + static override readonly type = 'render-markers-list'; + + constructor( + /** the list of markers to display, can be modified */ + public markers: Marker[] + ) { + super(RenderMarkersListEvent.type); + } +} + +export type MarkersPluginEvents = + | MarkerVisibilityEvent + | GotoMarkerDoneEvent + | LeaveMarkerEvent + | EnterMarkerEvent + | SelectMarkerEvent + | UnselectMarkerEvent + | HideMarkersEvent + | SetMarkersEvent + | ShowMarkersEvent + | RenderMarkersListEvent; diff --git a/src/plugins/markers/pin-list.svg b/packages/markers-plugin/src/icons/pin-list.svg similarity index 100% rename from src/plugins/markers/pin-list.svg rename to packages/markers-plugin/src/icons/pin-list.svg diff --git a/src/plugins/markers/pin.svg b/packages/markers-plugin/src/icons/pin.svg similarity index 100% rename from src/plugins/markers/pin.svg rename to packages/markers-plugin/src/icons/pin.svg diff --git a/packages/markers-plugin/src/index.ts b/packages/markers-plugin/src/index.ts new file mode 100644 index 000000000..668380965 --- /dev/null +++ b/packages/markers-plugin/src/index.ts @@ -0,0 +1,17 @@ +import { DEFAULTS, registerButton } from '@photo-sphere-viewer/core'; +import * as events from './events'; +import { MarkersButton } from './MarkersButton'; +import { MarkersListButton } from './MarkersListButton'; + +DEFAULTS.lang[MarkersButton.id] = 'Markers'; +DEFAULTS.lang[MarkersListButton.id] = 'Markers list'; +registerButton(MarkersButton, 'caption:left'); +registerButton(MarkersListButton, 'caption:left'); + +export type { Marker, MarkerType } from './Marker'; +export { MarkersPlugin } from './MarkersPlugin'; +export * from './model'; +export { events }; + +/** @internal */ +import './style.scss'; diff --git a/packages/markers-plugin/src/model.ts b/packages/markers-plugin/src/model.ts new file mode 100644 index 000000000..2044715f8 --- /dev/null +++ b/packages/markers-plugin/src/model.ts @@ -0,0 +1,175 @@ +import type { ExtendedPosition, Position, Size } from '@photo-sphere-viewer/core'; + +/** + * Configuration of a marker + */ +export type MarkerConfig = { + /** + * Path to the image representing the marker + */ + image?: string; + /** + * Path to the image representing the marker + */ + imageLayer?: string; + /** + * HTML content of the marker + */ + html?: string; + /** + * Size of the square + */ + square?: number; + /** + * Size of the rectangle + */ + rect?: [number, number] | { width: number; height: number }; + /** + * Radius of the circle + */ + circle?: number; + /** + * Radiuses of the ellipse + */ + ellipse?: [number, number] | { rx: number; ry: number }; + /** + * Definition of the path + */ + path?: string; + /** + * Array of points defining the polygon in spherical coordinates + */ + polygon?: [number, number][] | [string, string][] | number[] | string[]; + /** + * Array of points defining the polygon in pixel coordinates on the panorama image + */ + polygonPixels?: [number, number][] | number[]; + /** + * Array of points defining the polyline in spherical coordinates + */ + polyline?: [number, number][] | [string, string][] | number[] | string[]; + /** + * Array of points defining the polyline in pixel coordinates on the panorama image + */ + polylinePixels?: [number, number][] | number[]; + + /** + * @deprecated use `polygon` instead + */ + polygonRad?: [number, number][] | [string, string][] | number[] | string[]; + /** + * @deprecated use `polygonPixels` instead + */ + polygonPx?: [number, number][] | number[]; + /** + * @deprecated use `polyline` instead + */ + polylineRad?: [number, number][] | [string, string][] | number[] | string[]; + /** + * @deprecated use `polylinePixels` instead + */ + polylinePx?: [number, number][] | number[]; + + /** + * Unique identifier of the marker + */ + id: string; + /** + * Position of the marker (required but for `polygon` and `polyline`) + */ + position?: ExtendedPosition; + /** + * Size of the marker (required for `image` and `imageLayer`, recommended for `html`, ignored for others) + */ + size?: Size; + /** + * Applies a perspective on the image to make it look like placed on the floor or on a wall (only for `imageLayer`) + */ + orientation?: 'front' | 'horizontal' | 'vertical-left' | 'vertical-right'; + /** + * Configures the scale of the marker depending on the zoom level and/or the horizontal offset (ignored for `polygon`, `polyline` and `imageLayer`) + */ + scale?: + | [number, number] + | { zoom?: [number, number]; yaw?: [number, number] } + | ((zoomLevel: number, position: Position) => number); + /** + * Opacity of the marker + * @default 1 + */ + opacity?: number; + /** + * CSS class(es) added to the marker element (ignored for `imageLayer`) + */ + className?: string; + /** + * CSS properties to set on the marker (background, border, etc.) (ignored for `imagerLayer`) + */ + style?: Record; + /** + * SVG properties to set on the marker (fill, stroke, etc.) (only for SVG markers) + */ + svgStyle?: Record; + /** + * Defines where the marker is placed toward its defined position + * @default 'center center' + */ + anchor?: string; + /** + * The zoom level which will be applied when calling `gotoMarker()` method or when clicking on the marker in the list + * @default `current zoom level` + */ + zoomLvl?: number; + /** + * Initial visibility of the marker + * @default true + */ + visible?: boolean; + /** + * Configuration of the marker tooltip + * @default `{content: null, position: 'top center', className: null, trigger: 'hover'}` + */ + tooltip?: string | { content: string; position?: string; className?: string; trigger?: 'hover' | 'click' }; + /** + * HTML content that will be displayed on the side panel when the marker is clicked + */ + content?: string; + /** + * The name that appears in the list of markers + * @default `tooltip.content` + */ + listContent?: string; + /** + * Hide the marker in the markers list + * @default false + */ + hideList?: boolean; + /** + * Any custom data you want to attach to the marker + */ + data?: any; +}; + +export type ParsedMarkerConfig = Omit & { + scale?: + | { zoom?: [number, number]; yaw?: [number, number] } + | ((zoomLevel: number, position: Position) => number); + tooltip?: { content: string; position?: string; className?: string; trigger?: 'hover' | 'click' }; +}; + +export type MarkersPluginConfig = { + /** + * If a `click` event is triggered on the viewer additionally to the `select-marker` event + * @default false + */ + clickEventOnMarker?: boolean; + /** + * initial markers + */ + markers?: MarkerConfig[]; + /** + * Default animation speed for {@link MarkersPlugin#gotoMarker} + * @default '8rpm' + */ + gotoMarkerSpeed?: string | number; +}; diff --git a/packages/markers-plugin/src/style.scss b/packages/markers-plugin/src/style.scss new file mode 100644 index 000000000..a07188382 --- /dev/null +++ b/packages/markers-plugin/src/style.scss @@ -0,0 +1,45 @@ +@import '../../shared/src/vars'; + +.psv-markers { + user-select: none; + position: absolute; + z-index: $psv-hud-zindex; + width: 100%; + height: 100%; + + &-svg-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: $psv-polygon-marker-zindex; + } +} + +.psv-marker { + display: none; + + &--normal { + position: absolute; + top: 0; + left: 0; + z-index: $psv-marker-zindex; + background-size: contain; + background-repeat: no-repeat; + } + + &--transparent { + display: block; + opacity: 0; + } + + &--visible { + display: block; + } + + &--has-tooltip, + &--has-content { + cursor: pointer; + } +} diff --git a/packages/markers-plugin/src/utils.ts b/packages/markers-plugin/src/utils.ts new file mode 100644 index 000000000..6f2aad149 --- /dev/null +++ b/packages/markers-plugin/src/utils.ts @@ -0,0 +1,90 @@ +import { CONSTANTS, utils } from '@photo-sphere-viewer/core'; + +/** + * Returns intermediary point between two points on the sphere + * {@link http://www.movable-type.co.uk/scripts/latlong.html} + * @internal + */ +export function greatArcIntermediaryPoint(p1: [number, number], p2: [number, number], f: number): [number, number] { + const [λ1, φ1] = p1; + const [λ2, φ2] = p2; + + const r = utils.greatArcDistance(p1, p2); + const a = Math.sin((1 - f) * r) / Math.sin(r); + const b = Math.sin(f * r) / Math.sin(r); + const x = a * Math.cos(φ1) * Math.cos(λ1) + b * Math.cos(φ2) * Math.cos(λ2); + const y = a * Math.cos(φ1) * Math.sin(λ1) + b * Math.cos(φ2) * Math.sin(λ2); + const z = a * Math.sin(φ1) + b * Math.sin(φ2); + + return [Math.atan2(y, x), Math.atan2(z, Math.sqrt(x * x + y * y))]; +} + +/** + * Given a list of spherical points, offsets yaws in order to have only coutinuous values + * eg: [0.2, 6.08] is transformed to [0.2, -0.2] + */ +function getPolygonCoherentPoints(points: [number, number][]) { + const workPoints = [points[0]]; + + let k = 0; + for (let i = 1; i < points.length; i++) { + const d = points[i - 1][0] - points[i][0]; + if (d > Math.PI) { + // crossed the origin left to right + k += 1; + } else if (d < -Math.PI) { + // crossed the origin right to left + k -= 1; + } + workPoints.push([points[i][0] + k * 2 * Math.PI, points[i][1]]); + } + + return workPoints; +} + +/** + * Computes the center point of a polygon + * @todo Get "visual center" (https://blog.mapbox.com/a-new-algorithm-for-finding-a-visual-center-of-a-polygon-7c77e6492fbc) + * @internal + */ +export function getPolygonCenter(polygon: [number, number][]): [number, number] { + const points = getPolygonCoherentPoints(polygon); + + const sum = points.reduce((intermediary, point) => [intermediary[0] + point[0], intermediary[1] + point[1]]); + return [utils.parseAngle(sum[0] / polygon.length), sum[1] / polygon.length]; +} + +/** + * Computes the middle point of a polyline + * @internal + */ +export function getPolylineCenter(polyline: [number, number][]): [number, number] { + const points = getPolygonCoherentPoints(polyline); + + // compute each segment length + total length + let length = 0; + const lengths = []; + + for (let i = 0; i < points.length - 1; i++) { + const l = utils.greatArcDistance(points[i], points[i + 1]) * CONSTANTS.SPHERE_RADIUS; + + lengths.push(l); + length += l; + } + + // iterate until length / 2 + let consumed = 0; + + for (let j = 0; j < points.length - 1; j++) { + // once the segment containing the middle point is found, computes the intermediary point + if (consumed + lengths[j] > length / 2) { + const r = (length / 2 - consumed) / lengths[j]; + return greatArcIntermediaryPoint(points[j], points[j + 1], r); + } + + consumed += lengths[j]; + } + + // this never happens + return points[Math.round(points.length / 2)]; +} diff --git a/packages/markers-plugin/tsconfig.json b/packages/markers-plugin/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/markers-plugin/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/markers-plugin/tsup.config.js b/packages/markers-plugin/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/markers-plugin/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/resolution-plugin/.typedoc/README.md b/packages/resolution-plugin/.typedoc/README.md new file mode 100644 index 000000000..f5c253a72 --- /dev/null +++ b/packages/resolution-plugin/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/resolution-plugin](https://www.npmjs.com/package/@photo-sphere-viewer/resolution-plugin) + +Documentation : https://photo-sphere-viewer.js.org/plugins/resolution diff --git a/packages/resolution-plugin/package.json b/packages/resolution-plugin/package.json new file mode 100644 index 000000000..ba12ea698 --- /dev/null +++ b/packages/resolution-plugin/package.json @@ -0,0 +1,26 @@ +{ + "name": "@photo-sphere-viewer/resolution-plugin", + "version": "0.0.0", + "description": "Photo sphere Viewer plugin that adds a setting to choose between multiple resolutions of the panorama.", + "homepage": "https://photo-sphere-viewer.js.org/plugins/resolution", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0", + "@photo-sphere-viewer/settings-plugin": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.ResolutionPlugin" + }, + "typedoc": { + "displayName": "plugin: Resolution", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/resolution-plugin/src/ResolutionPlugin.ts b/packages/resolution-plugin/src/ResolutionPlugin.ts new file mode 100644 index 000000000..cdde216a5 --- /dev/null +++ b/packages/resolution-plugin/src/ResolutionPlugin.ts @@ -0,0 +1,174 @@ +import type { Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin, events, PSVError, utils } from '@photo-sphere-viewer/core'; +import type { OptionsSetting, SettingsPlugin } from '@photo-sphere-viewer/settings-plugin'; +import { ResolutionChangedEvent, ResolutionPluginEvents } from './events'; +import { Resolution, ResolutionPluginConfig } from './model'; + +const getConfig = utils.getConfigParser({ + resolutions: null, + defaultResolution: null, + showBadge: true, +}); + +/** + * Adds a setting to choose between multiple resolutions of the panorama. + */ +export class ResolutionPlugin extends AbstractPlugin { + static override readonly id = 'resolution'; + + readonly config: ResolutionPluginConfig; + + private resolutions: Resolution[] = []; + private resolutionsById: Record = {}; + + private readonly state = { + resolution: null as string, + }; + + private settings: SettingsPlugin; + + constructor(viewer: Viewer, config: ResolutionPluginConfig) { + super(viewer); + + this.config = getConfig(config); + + if (this.config.defaultResolution && this.viewer.config.panorama) { + utils.logWarn( + 'ResolutionPlugin, a defaultResolution was provided ' + + 'but a panorama is already configured on the viewer, ' + + 'the defaultResolution will be ignored.' + ); + } + } + + /** + * @internal + */ + override init() { + super.init(); + + this.settings = this.viewer.getPlugin('settings'); + + if (!this.settings) { + throw new PSVError('Resolution plugin requires the Settings plugin'); + } + + this.settings.addSetting({ + id: ResolutionPlugin.id, + type: 'options', + label: this.viewer.config.lang.resolution, + current: () => this.state.resolution, + options: () => this.resolutions, + apply: (resolution) => this.__setResolutionIfExists(resolution), + badge: !this.config.showBadge ? null : () => this.state.resolution, + }); + + this.viewer.addEventListener(events.PanoramaLoadedEvent.type, this); + + if (this.config.resolutions) { + this.setResolutions( + this.config.resolutions, + this.viewer.config.panorama ? null : this.config.defaultResolution + ); + delete this.config.resolutions; + delete this.config.defaultResolution; + } + } + + /** + * @internal + */ + override destroy() { + this.viewer.removeEventListener(events.PanoramaLoadedEvent.type, this); + + this.settings.removeSetting(ResolutionPlugin.id); + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof events.PanoramaLoadedEvent) { + this.__refreshResolution(); + } + } + + /** + * Changes the available resolutions + * @param resolutions + * @param defaultResolution - if not provided, the current panorama is kept + * @throws {@link PSVError} if the configuration is invalid + */ + setResolutions(resolutions: Resolution[], defaultResolution?: string) { + this.resolutions = resolutions || []; + this.resolutionsById = {}; + + resolutions.forEach((resolution) => { + if (!resolution.id) { + throw new PSVError('Missing resolution id'); + } + if (!resolution.panorama) { + throw new PSVError('Missing resolution panorama'); + } + this.resolutionsById[resolution.id] = resolution; + }); + + // pick first resolution if no default provided and no current panorama + if (!this.viewer.config.panorama && !defaultResolution) { + defaultResolution = resolutions[0].id; + } + + // ensure the default resolution exists + if (defaultResolution && !this.resolutionsById[defaultResolution]) { + utils.logWarn(`Resolution ${defaultResolution} unknown`); + defaultResolution = resolutions[0].id; + } + + if (defaultResolution) { + this.setResolution(defaultResolution); + } + + this.__refreshResolution(); + } + + /** + * Changes the current resolution + * @throws {@link PSVError} if the resolution does not exist + */ + setResolution(id: string): Promise { + if (!this.resolutionsById[id]) { + throw new PSVError(`Resolution ${id} unknown`); + } + + return this.__setResolutionIfExists(id); + } + + private __setResolutionIfExists(id: string): Promise { + if (this.resolutionsById[id]) { + return this.viewer.setPanorama(this.resolutionsById[id].panorama, { transition: false, showLoader: false }); + } else { + return Promise.resolve(); + } + } + + /** + * Returns the current resolution + */ + getResolution(): string { + return this.state.resolution; + } + + /** + * Updates current resolution on panorama load + */ + private __refreshResolution() { + const resolution = this.resolutions.find((r) => utils.deepEqual(this.viewer.config.panorama, r.panorama)); + if (this.state.resolution !== resolution?.id) { + this.state.resolution = resolution?.id; + this.settings?.updateButton(); + this.dispatchEvent(new ResolutionChangedEvent(this.state.resolution)); + } + } +} diff --git a/packages/resolution-plugin/src/events.ts b/packages/resolution-plugin/src/events.ts new file mode 100644 index 000000000..819e63d54 --- /dev/null +++ b/packages/resolution-plugin/src/events.ts @@ -0,0 +1,15 @@ +import { TypedEvent } from '@photo-sphere-viewer/core'; +import type { ResolutionPlugin } from './ResolutionPlugin'; + +/** + * @event Triggered when the resolution is changed + */ +export class ResolutionChangedEvent extends TypedEvent { + static override readonly type = 'resolution-changed'; + + constructor(public readonly resolutionId: string) { + super(ResolutionChangedEvent.type); + } +} + +export type ResolutionPluginEvents = ResolutionChangedEvent; diff --git a/packages/resolution-plugin/src/index.ts b/packages/resolution-plugin/src/index.ts new file mode 100644 index 000000000..a2f4eba84 --- /dev/null +++ b/packages/resolution-plugin/src/index.ts @@ -0,0 +1,8 @@ +import { DEFAULTS } from '@photo-sphere-viewer/core'; +import * as events from './events'; + +DEFAULTS.lang.resolution = 'Quality'; + +export { ResolutionPlugin } from './ResolutionPlugin'; +export * from './model'; +export { events }; diff --git a/packages/resolution-plugin/src/model.ts b/packages/resolution-plugin/src/model.ts new file mode 100644 index 000000000..7763341e5 --- /dev/null +++ b/packages/resolution-plugin/src/model.ts @@ -0,0 +1,21 @@ +export type Resolution = { + id: string; + label: string; + panorama: any; +}; + +export type ResolutionPluginConfig = { + /** + * list of available resolutions + */ + resolutions: Resolution[]; + /** + * the default resolution if no panorama is configured on the viewer + */ + defaultResolution?: string; + /** + * show the resolution id as a badge on the settings button + * @default true + */ + showBadge?: boolean; +}; diff --git a/packages/resolution-plugin/tsconfig.json b/packages/resolution-plugin/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/resolution-plugin/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/resolution-plugin/tsup.config.js b/packages/resolution-plugin/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/resolution-plugin/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/settings-plugin/.typedoc/README.md b/packages/settings-plugin/.typedoc/README.md new file mode 100644 index 000000000..c070a7e5c --- /dev/null +++ b/packages/settings-plugin/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/settings-plugin](https://www.npmjs.com/package/@photo-sphere-viewer/settings-plugin) + +Documentation : https://photo-sphere-viewer.js.org/plugins/settings diff --git a/packages/settings-plugin/package.json b/packages/settings-plugin/package.json new file mode 100644 index 000000000..a6b4dce01 --- /dev/null +++ b/packages/settings-plugin/package.json @@ -0,0 +1,26 @@ +{ + "name": "@photo-sphere-viewer/settings-plugin", + "version": "0.0.0", + "description": "Photo sphere Viewer configurable plugin that adds a button to access various settings.", + "homepage": "https://photo-sphere-viewer.js.org/plugins/settings", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix && stylelint \"src/**/*.scss\" --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.SettingsPlugin", + "style": true + }, + "typedoc": { + "displayName": "plugin: Settings", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/settings-plugin/src/SettingsButton.ts b/packages/settings-plugin/src/SettingsButton.ts new file mode 100644 index 000000000..9b54f9564 --- /dev/null +++ b/packages/settings-plugin/src/SettingsButton.ts @@ -0,0 +1,47 @@ +import type { Navbar } from '@photo-sphere-viewer/core'; +import { AbstractButton } from '@photo-sphere-viewer/core'; +import icon from './icons/settings.svg'; +import type { SettingsPlugin } from './SettingsPlugin'; + +export class SettingsButton extends AbstractButton { + static override readonly id = 'settings'; + + private readonly plugin: SettingsPlugin; + private readonly badge: HTMLElement; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-settings-button', + icon: icon, + hoverScale: true, + collapsable: false, + tabbable: true, + }); + + this.plugin = this.viewer.getPlugin('settings'); + + this.badge = document.createElement('div'); + this.badge.className = 'psv-settings-badge'; + this.badge.style.display = 'none'; + this.container.appendChild(this.badge); + } + + override isSupported() { + return !!this.plugin; + } + + /** + * Toggles settings + */ + onClick() { + this.plugin.toggleSettings(); + } + + /** + * Changes the badge value + */ + setBadge(value: string) { + this.badge.innerText = value; + this.badge.style.display = value ? '' : 'none'; + } +} diff --git a/packages/settings-plugin/src/SettingsComponent.ts b/packages/settings-plugin/src/SettingsComponent.ts new file mode 100644 index 000000000..ad8d593cb --- /dev/null +++ b/packages/settings-plugin/src/SettingsComponent.ts @@ -0,0 +1,142 @@ +import type { Viewer } from '@photo-sphere-viewer/core'; +import { AbstractComponent, CONSTANTS, utils } from '@photo-sphere-viewer/core'; +import { ID_BACK, ID_ENTER, OPTION_DATA, SETTINGS_TEMPLATE, SETTING_DATA, SETTING_OPTIONS_TEMPLATE } from './constants'; +import { OptionsSetting, ToggleSetting } from './model'; +import type { SettingsPlugin } from './SettingsPlugin'; + +export class SettingsComponent extends AbstractComponent { + constructor(private readonly plugin: SettingsPlugin, viewer: Viewer) { + super(viewer, { + className: 'psv-settings psv--capture-event', + }); + + this.container.addEventListener('click', this); + this.container.addEventListener('transitionend', this); + this.container.addEventListener('keydown', this); + + this.hide(); + } + + handleEvent(e: Event) { + switch (e.type) { + case 'click': + this.__click(e.target as HTMLElement); + break; + + case 'transitionend': + if (!this.isVisible()) { + this.container.innerHTML = ''; // empty content after fade out + } else { + this.__focusFirstOption(); + } + break; + + case 'keydown': + if (this.isVisible()) { + switch ((e as KeyboardEvent).key) { + case CONSTANTS.KEY_CODES.Escape: + this.plugin.hideSettings(); + break; + case CONSTANTS.KEY_CODES.Enter: + this.__click(e.target as HTMLElement); + break; + } + } + break; + } + } + + override show() { + this.__showSettings(false); + + this.container.classList.add('psv-settings--open'); + this.state.visible = true; + } + + override hide() { + this.container.classList.remove('psv-settings--open'); + this.state.visible = false; + } + + /** + * Handle clicks on items + */ + private __click(element: HTMLElement) { + const li = utils.getClosest(element, 'li'); + if (!li) { + return; + } + + const settingId = li.dataset[SETTING_DATA]; + const optionId = li.dataset[OPTION_DATA]; + + const setting = this.plugin.settings.find((s) => s.id === settingId); + + switch (optionId) { + case ID_BACK: + this.__showSettings(true); + break; + + case ID_ENTER: + switch (setting.type) { + case 'toggle': + this.plugin.toggleSettingValue(setting as ToggleSetting); + this.__showSettings(true); // re-render + break; + + case 'options': + this.__showOptions(setting as OptionsSetting); + break; + + default: + // noop + } + break; + + default: + switch (setting.type) { + case 'options': + this.hide(); + this.plugin.applySettingOption(setting as OptionsSetting, optionId); + break; + + default: + // noop + } + break; + } + } + + /** + * Shows the list of options + */ + private __showSettings(focus: boolean) { + this.container.innerHTML = SETTINGS_TEMPLATE(this.plugin.settings, (setting) => { + const current = setting.current(); + const option = setting.options().find((opt) => opt.id === current); + return option?.label; + }); + + // must not focus during the initial transition + if (focus) { + this.__focusFirstOption(); + } + } + + /** + * Shows setting options panel + */ + private __showOptions(setting: OptionsSetting) { + const current = setting.current(); + + this.container.innerHTML = SETTING_OPTIONS_TEMPLATE(setting, (option) => { + return option.id === current; + }); + + this.__focusFirstOption(); + } + + private __focusFirstOption() { + this.container.querySelector('[tabindex]')?.focus(); + } +} diff --git a/packages/settings-plugin/src/SettingsPlugin.ts b/packages/settings-plugin/src/SettingsPlugin.ts new file mode 100644 index 000000000..c38da38f3 --- /dev/null +++ b/packages/settings-plugin/src/SettingsPlugin.ts @@ -0,0 +1,224 @@ +import type { Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin, events, PSVError, utils } from '@photo-sphere-viewer/core'; +import { LOCAL_STORAGE_KEY } from './constants'; +import { SettingChangedEvent, SettingsPluginEvents } from './events'; +import { OptionsSetting, Setting, SettingsPluginConfig, ToggleSetting } from './model'; +import { SettingsButton } from './SettingsButton'; +import { SettingsComponent } from './SettingsComponent'; + +function getData() { + return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || {}; +} + +function setData(data: any) { + localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(data)); +} + +const getConfig = utils.getConfigParser({ + persist: false, + storage: { + set(settingId: string, value: boolean | string) { + const data = getData(); + data[settingId] = value; + setData(data); + }, + get(settingId: string) { + return getData()[settingId]; + }, + }, +}); + +/** + * Adds a button to access various settings + */ +export class SettingsPlugin extends AbstractPlugin { + static override readonly id = 'settings'; + + readonly config: SettingsPluginConfig; + + private readonly component: SettingsComponent; + readonly settings: Setting[] = []; + + constructor(viewer: Viewer, config: SettingsPluginConfig) { + super(viewer); + + this.config = getConfig(config); + + this.component = new SettingsComponent(this, this.viewer); + } + + /** + * @internal + */ + override init() { + super.init(); + + this.viewer.addEventListener(events.ClickEvent.type, this); + this.viewer.addEventListener(events.ShowPanelEvent.type, this); + + // buttons are initialized just after plugins + setTimeout(() => this.updateButton()); + } + + /** + * @internal + */ + override destroy() { + this.viewer.removeEventListener(events.ClickEvent.type, this); + this.viewer.removeEventListener(events.ShowPanelEvent.type, this); + + this.component.destroy(); + this.settings.length = 0; + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof events.ClickEvent || e instanceof events.ShowPanelEvent) { + if (this.component.isVisible()) { + this.hideSettings(); + } + } + } + + /** + * Registers a new setting + * @throws {@link PSVError} if the configuration is invalid + */ + addSetting(setting: Setting) { + if (!setting.id) { + throw new PSVError('Missing setting id'); + } + if (!setting.type) { + throw new PSVError('Missing setting type'); + } + + if (setting.badge && this.settings.some((s) => s.badge)) { + utils.logWarn('More than one setting with a badge are declared, the result is unpredictable.'); + } + + this.settings.push(setting); + + if (this.component.isVisible()) { + this.component.show(); // re-render + } + + this.updateButton(); + + if (this.config.persist) { + Promise.resolve(this.config.storage.get(setting.id)).then((value) => { + switch (setting.type) { + case 'toggle': { + const toggle = setting as ToggleSetting; + if (!utils.isNil(value) && value !== toggle.active()) { + toggle.toggle(); + this.dispatchEvent(new SettingChangedEvent(toggle.id, toggle.active())); + } + break; + } + + case 'options': { + const options = setting as OptionsSetting; + if (!utils.isNil(value) && value !== options.current()) { + options.apply(value as string); + this.dispatchEvent(new SettingChangedEvent(options.id, options.current())); + } + break; + } + + default: + // noop + } + + this.updateButton(); + }); + } + } + + /** + * Removes a setting + */ + removeSetting(id: string) { + const idx = this.settings.findIndex((setting) => setting.id === id); + if (idx !== -1) { + this.settings.splice(idx, 1); + + if (this.component.isVisible()) { + this.component.show(); // re-render + } + + this.updateButton(); + } + } + + /** + * Toggles the settings menu + */ + toggleSettings() { + this.component.toggle(); + this.updateButton(); + } + + /** + * Hides the settings menu + */ + hideSettings() { + this.component.hide(); + this.updateButton(); + } + + /** + * Shows the settings menu + */ + showSettings() { + this.component.show(); + this.updateButton(); + } + + /** + * Updates the badge in the button + */ + updateButton() { + const value = this.settings.find((s) => s.badge)?.badge(); + const button = this.viewer.navbar.getButton(SettingsButton.id, false) as SettingsButton; + button?.toggleActive(this.component.isVisible()); + button?.setBadge(value); + } + + /** + * Toggles a setting + * @internal + */ + toggleSettingValue(setting: ToggleSetting) { + const newValue = !setting.active(); // in case "toggle" is async + + setting.toggle(); + + this.dispatchEvent(new SettingChangedEvent(setting.id, newValue)); + + if (this.config.persist) { + this.config.storage.set(setting.id, newValue); + } + + this.updateButton(); + } + + /** + * Changes the value of an setting + * @internal + */ + applySettingOption(setting: OptionsSetting, optionId: string) { + setting.apply(optionId); + + this.dispatchEvent(new SettingChangedEvent(setting.id, optionId)); + + if (this.config.persist) { + this.config.storage.set(setting.id, optionId); + } + + this.updateButton(); + } +} diff --git a/packages/settings-plugin/src/constants.ts b/packages/settings-plugin/src/constants.ts new file mode 100644 index 000000000..47cfeef19 --- /dev/null +++ b/packages/settings-plugin/src/constants.ts @@ -0,0 +1,64 @@ +import { utils } from '@photo-sphere-viewer/core'; +import check from './icons/check.svg'; +import chevron from './icons/chevron.svg'; +import switchOff from './icons/switch-off.svg'; +import switchOn from './icons/switch-on.svg'; +import { OptionsSetting, BaseSetting, SettingOption, ToggleSetting } from './model'; + +export const LOCAL_STORAGE_KEY = 'psvSettings'; +export const ID_PANEL = 'settings'; +export const SETTING_DATA = 'settingId'; +export const OPTION_DATA = 'optionId'; +export const ID_BACK = '__back'; +export const ID_ENTER = '__enter'; +export const SETTING_DATA_KEY = utils.dasherize(SETTING_DATA); +export const OPTION_DATA_KEY = utils.dasherize(OPTION_DATA); + +/** + * Setting item template, by type + */ +export const SETTINGS_TEMPLATE_: Record = { + options: (setting: OptionsSetting, optionsCurrent: (s: OptionsSetting) => string) => ` +${setting.label} +${optionsCurrent(setting)} +${chevron} +`, + toggle: (setting: ToggleSetting) => ` +${setting.label} +${setting.active() ? switchOn : switchOff} +`, +}; + +/** + * Settings list template + */ +export const SETTINGS_TEMPLATE = (settings: BaseSetting[], optionsCurrent: (s: OptionsSetting) => string) => ` +
    + ${settings.map((setting) => ` +
  • + ${SETTINGS_TEMPLATE_[setting.type](setting as OptionsSetting, optionsCurrent)} +
  • + `).join('')} +
+`; + +/** + * Settings options template + */ +export const SETTING_OPTIONS_TEMPLATE = (setting: OptionsSetting, optionActive: (o: SettingOption) => boolean) => ` +
    +
  • + ${chevron} + ${setting.label} +
  • + ${setting.options().map((option) => ` +
  • + ${optionActive(option) ? check : ''} + ${option.label} +
  • + `).join('')} +
+`; diff --git a/packages/settings-plugin/src/events.ts b/packages/settings-plugin/src/events.ts new file mode 100644 index 000000000..cd0953754 --- /dev/null +++ b/packages/settings-plugin/src/events.ts @@ -0,0 +1,12 @@ +import { TypedEvent } from '@photo-sphere-viewer/core'; +import type { SettingsPlugin } from './SettingsPlugin'; + +export class SettingChangedEvent extends TypedEvent { + static override readonly type = 'setting-changed'; + + constructor(public readonly settingId: string, public readonly settingValue: boolean | string) { + super(SettingChangedEvent.type); + } +} + +export type SettingsPluginEvents = SettingChangedEvent; diff --git a/src/plugins/settings/check.svg b/packages/settings-plugin/src/icons/check.svg similarity index 100% rename from src/plugins/settings/check.svg rename to packages/settings-plugin/src/icons/check.svg diff --git a/src/plugins/settings/chevron.svg b/packages/settings-plugin/src/icons/chevron.svg similarity index 100% rename from src/plugins/settings/chevron.svg rename to packages/settings-plugin/src/icons/chevron.svg diff --git a/src/plugins/settings/settings.svg b/packages/settings-plugin/src/icons/settings.svg similarity index 100% rename from src/plugins/settings/settings.svg rename to packages/settings-plugin/src/icons/settings.svg diff --git a/src/plugins/settings/switch-off.svg b/packages/settings-plugin/src/icons/switch-off.svg similarity index 100% rename from src/plugins/settings/switch-off.svg rename to packages/settings-plugin/src/icons/switch-off.svg diff --git a/src/plugins/settings/switch-on.svg b/packages/settings-plugin/src/icons/switch-on.svg similarity index 100% rename from src/plugins/settings/switch-on.svg rename to packages/settings-plugin/src/icons/switch-on.svg diff --git a/packages/settings-plugin/src/index.ts b/packages/settings-plugin/src/index.ts new file mode 100644 index 000000000..7f4dc6775 --- /dev/null +++ b/packages/settings-plugin/src/index.ts @@ -0,0 +1,13 @@ +import { DEFAULTS, registerButton } from '@photo-sphere-viewer/core'; +import * as events from './events'; +import { SettingsButton } from './SettingsButton'; + +DEFAULTS.lang[SettingsButton.id] = 'Settings'; +registerButton(SettingsButton, 'fullscreen:left'); + +export { SettingsPlugin } from './SettingsPlugin'; +export * from './model'; +export { events }; + +/** @internal */ +import './style.scss'; diff --git a/packages/settings-plugin/src/model.ts b/packages/settings-plugin/src/model.ts new file mode 100644 index 000000000..d448977cd --- /dev/null +++ b/packages/settings-plugin/src/model.ts @@ -0,0 +1,90 @@ +/** + * Description of a setting + */ +export type BaseSetting = { + /** + * identifier of the setting + */ + id: string; + /** + * label of the setting + */ + label: string; + /** + * type of the setting + */ + type: 'options' | 'toggle'; + /** + * function which returns the value of the button badge + */ + badge?(): string; +}; + +/** + * Description of a 'options' setting + */ +export type OptionsSetting = BaseSetting & { + type: 'options'; + /** + * function which returns the current option id + */ + current(): string; + /** + * function which the possible options + */ + options(): SettingOption[]; + /** + * function called with the id of the selected option + */ + apply(optionId: string): void; +}; + +/** + * Description of a 'toggle' setting + */ +export type ToggleSetting = BaseSetting & { + type: 'toggle'; + /** + * function which return whereas the setting is active or not + */ + active(): boolean; + /** + * function called when the setting is toggled + */ + toggle(): void; +}; + +/** + * Option for an 'options' setting + */ +export type SettingOption = { + /** + * identifier of the option + */ + id: string; + /** + * label of the option + */ + label: string; +}; + +export type Setting = ToggleSetting | OptionsSetting; + +export type SettingsPluginConfig = { + /** + * should the settings be saved accross sessions + * @default false + */ + persist?: boolean; + /** + * custom storage handler, defaults to LocalStorage + * @default LocalStorage + */ + storage?: { + set(settingId: string, value: boolean | string): void; + /** + * return `undefined` or `null` if the option does not exist + */ + get(settingId: string): boolean | string | Promise | Promise; + }; +}; diff --git a/packages/settings-plugin/src/style.scss b/packages/settings-plugin/src/style.scss new file mode 100644 index 000000000..77b2e3c4d --- /dev/null +++ b/packages/settings-plugin/src/style.scss @@ -0,0 +1,96 @@ +@use 'sass:list'; +@import '../../shared/src/vars'; + +$psv-settings-margin: 10px !default; +$psv-settings-item-height: $psv-panel-menu-item-height !default; +$psv-settings-item-padding: $psv-panel-menu-item-padding !default; +$psv-settings-background: $psv-panel-background !default; +$psv-settings-shadow: 0 0 5px $psv-settings-background !default; +$psv-settings-text-color: $psv-panel-text-color !default; +$psv-settings-hover-background: $psv-panel-menu-hover-background !default; +$psv-settings-badge-font: 10px / 0.9 monospace !default; +$psv-settings-badge-background: #111 !default; +$psv-settings-badge-text-color: white !default; + +.psv-settings { + position: absolute; + bottom: $psv-navbar-height + $psv-settings-margin; + right: $psv-settings-margin; + background: $psv-settings-background; + box-shadow: $psv-settings-shadow; + color: $psv-settings-text-color; + z-index: $psv-navbar-zindex; + opacity: 0; + transition: opacity 0.1s linear; + + &--open { + opacity: 1; + } + + &-list { + list-style: none; + margin: 0; + padding: 0; + } +} + +.psv-settings-item { + height: $psv-settings-item-height; + padding: $psv-settings-item-padding; + display: flex; + align-items: center; + justify-content: flex-start; + cursor: pointer; + + &:hover { + background: $psv-settings-hover-background; + } + + &:focus-visible { + outline: $psv-element-focus-outline; + outline-offset: -#{list.nth($psv-element-focus-outline, 1)}; + } + + *:not(:last-child) { + margin-right: 1em; + } + + &-label { + flex: 1; + font-weight: bold; + } + + &-value { + flex: none; + } + + &-icon { + flex: none; + height: 1em; + width: 1em; + + svg { + width: 1em; + height: 1em; + } + } + + &--header { + border-bottom: 1px solid currentcolor; + + svg { + transform: scaleX(-1); + } + } +} + +.psv-settings-badge { + position: absolute; + top: 10%; + right: 10%; + border-radius: 0.2em; + padding: 0.2em; + background: $psv-settings-badge-background; + color: $psv-settings-badge-text-color; + font: $psv-settings-badge-font; +} diff --git a/packages/settings-plugin/tsconfig.json b/packages/settings-plugin/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/settings-plugin/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/settings-plugin/tsup.config.js b/packages/settings-plugin/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/settings-plugin/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/shared/.typedoc/README.md b/packages/shared/.typedoc/README.md new file mode 100644 index 000000000..59ddaf29e --- /dev/null +++ b/packages/shared/.typedoc/README.md @@ -0,0 +1 @@ +This module is not published but included in other packages. diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 000000000..c89e4f862 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,22 @@ +{ + "name": "@photo-sphere-viewer/shared", + "version": "0.0.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "scripts": { + "build:dev": "tsup --define.config dev", + "lint": "eslint . --fix && stylelint \"src/**/*.scss\" --fix", + "test": "mocha -r ts-node/register \"src/**/*.spec.ts\"" + }, + "psv": { + "globalName": "PhotoSphereViewer.Shared" + }, + "typedoc": { + "displayName": "Shared", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/shared/src/AbstractVideoAdapter.ts b/packages/shared/src/AbstractVideoAdapter.ts new file mode 100644 index 000000000..c9ff971b7 --- /dev/null +++ b/packages/shared/src/AbstractVideoAdapter.ts @@ -0,0 +1,203 @@ +import type { TextureData, Viewer } from '@photo-sphere-viewer/core'; +import { AbstractAdapter, events, PSVError } from '@photo-sphere-viewer/core'; +import { BufferGeometry, Material, Mesh, VideoTexture } from 'three'; + +export type AbstractVideoPanorama = { + source: string; +}; + +export type AbstractVideoAdapterConfig = { + /** + * automatically start the video + * @default false + */ + autoplay?: boolean; + /** + * initially mute the video + * @default false + */ + muted?: boolean; +}; + +type AbstractVideoMesh = Mesh; +type AbstractVideoTexture = TextureData; + +/** + * Base video adapters class + */ +export abstract class AbstractVideoAdapter extends AbstractAdapter< + TPanorama, + VideoTexture +> { + static override readonly supportsDownload = false; + static override readonly supportsOverlay = false; + + protected abstract readonly config: AbstractVideoAdapterConfig; + + private video: HTMLVideoElement; + + constructor(viewer: Viewer) { + super(viewer); + + this.viewer.addEventListener(events.BeforeRenderEvent.type, this); + } + + override destroy() { + this.viewer.removeEventListener(events.BeforeRenderEvent.type, this); + + this.__removeVideo(); + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof events.BeforeRenderEvent) { + this.viewer.needsUpdate(); + } + } + + override supportsPreload(): boolean { + return false; + } + + override supportsTransition(): boolean { + return false; + } + + loadTexture(panorama: AbstractVideoPanorama): Promise { + if (typeof panorama !== 'object' || !panorama.source) { + return Promise.reject(new PSVError('Invalid panorama configuration, are you using the right adapter?')); + } + + if (!this.viewer.getPlugin('video')) { + return Promise.reject(new PSVError('Video adapters require VideoPlugin to be loaded too.')); + } + + const video = this.__createVideo(panorama.source); + + return this.__videoLoadPromise(video).then(() => { + const texture = new VideoTexture(video); + return { panorama, texture }; + }); + } + + protected switchVideo(texture: VideoTexture) { + let currentTime; + let duration; + let paused = !this.config.autoplay; + let muted = this.config.muted; + let volume = 1; + if (this.video) { + ({ currentTime, duration, paused, muted, volume } = this.video); + } + + this.__removeVideo(); + this.video = texture.image; + + // keep current time when switching resolution + if (this.video.duration === duration) { + this.video.currentTime = currentTime; + } + + // keep volume + this.video.muted = muted; + this.video.volume = volume; + + // play + if (!paused) { + this.video.play(); + } + } + + setTextureOpacity(mesh: AbstractVideoMesh, opacity: number) { + mesh.material.opacity = opacity; + mesh.material.transparent = opacity < 1; + } + + setOverlay() { + throw new PSVError('VideoAdapter does not support overlay'); + } + + disposeTexture(textureData: AbstractVideoTexture) { + if (textureData.texture) { + const video: HTMLVideoElement = textureData.texture.image; + video.pause(); + this.viewer.container.removeChild(video); + } + textureData.texture?.dispose(); + } + + private __removeVideo() { + if (this.video) { + this.video.pause(); + this.viewer.container.removeChild(this.video); + delete this.video; + } + } + + private __createVideo(src: string): HTMLVideoElement { + const video = document.createElement('video'); + video.crossOrigin = this.viewer.config.withCredentials ? 'use-credentials' : 'anonymous'; + video.loop = true; + video.playsInline = true; + video.style.display = 'none'; + video.muted = this.config.muted; + video.src = src; + video.preload = 'metadata'; + + this.viewer.container.appendChild(video); + + return video; + } + + private __videoLoadPromise(video: HTMLVideoElement): Promise { + return new Promise((resolve, reject) => { + const onLoaded = () => { + if (this.video && video.duration === this.video.duration) { + resolve(this.__videoBufferPromise(video, this.video.currentTime)); + } else { + resolve(); + } + video.removeEventListener('loadedmetadata', onLoaded); + }; + + const onError = (err: ErrorEvent) => { + reject(err); + video.removeEventListener('error', onError); + }; + + video.addEventListener('loadedmetadata', onLoaded); + video.addEventListener('error', onError); + }); + } + + private __videoBufferPromise(video: HTMLVideoElement, currentTime: number): Promise { + return new Promise((resolve) => { + function onBuffer() { + const buffer = video.buffered; + for (let i = 0, l = buffer.length; i < l; i++) { + if (buffer.start(i) <= video.currentTime && buffer.end(i) >= video.currentTime) { + video.pause(); + video.removeEventListener('buffer', onBuffer); + video.removeEventListener('progress', onBuffer); + resolve(); + break; + } + } + } + + // try to reduce the switching time by preloading in advance + // FIXME find a better way ? + video.currentTime = Math.min(currentTime + 2000, video.duration); + video.muted = true; + + video.addEventListener('buffer', onBuffer); + video.addEventListener('progress', onBuffer); + + video.play(); + }); + } +} diff --git a/packages/shared/src/Queue.ts b/packages/shared/src/Queue.ts new file mode 100644 index 000000000..d71d6160e --- /dev/null +++ b/packages/shared/src/Queue.ts @@ -0,0 +1,62 @@ +import { Status, Task } from './Task'; + +/** + * @internal + */ +export class Queue { + private runningTasks: Record = {}; + private tasks: Record = {}; + + constructor(private readonly concurency = 4) {} + + enqueue(task: Task) { + this.tasks[task.id] = task; + } + + clear() { + Object.values(this.tasks).forEach((task) => task.cancel()); + this.tasks = {}; + this.runningTasks = {}; + } + + setPriority(taskId: string, priority: number) { + const task = this.tasks[taskId]; + if (task) { + task.priority = priority; + if (task.status === Status.DISABLED) { + task.status = Status.PENDING; + } + } + } + + disableAllTasks() { + Object.values(this.tasks).forEach((task) => { + task.status = Status.DISABLED; + }); + } + + start() { + if (Object.keys(this.runningTasks).length >= this.concurency) { + return; + } + + const nextTask = Object.values(this.tasks) + .filter((task) => task.status === Status.PENDING) + .sort((a, b) => b.priority - a.priority) + .pop(); + + if (nextTask) { + this.runningTasks[nextTask.id] = true; + + nextTask.start().then(() => { + if (!nextTask.isCancelled()) { + delete this.tasks[nextTask.id]; + delete this.runningTasks[nextTask.id]; + this.start(); + } + }); + + this.start(); // start tasks until max concurrency is reached + } + } +} diff --git a/packages/shared/src/Task.ts b/packages/shared/src/Task.ts new file mode 100644 index 000000000..c663a8317 --- /dev/null +++ b/packages/shared/src/Task.ts @@ -0,0 +1,44 @@ +/** + * @internal + */ +export const enum Status { + DISABLED, + PENDING, + RUNNING, + CANCELLED, + DONE, + ERROR, +} + +/** + * @internal + */ +export class Task { + status: Status = Status.PENDING; + + constructor( + public readonly id: string, + public priority: number, + private readonly fn: (task: Task) => Promise + ) {} + + start() { + this.status = Status.RUNNING; + return this.fn(this).then( + () => { + this.status = Status.DONE; + }, + () => { + this.status = Status.ERROR; + } + ); + } + + cancel() { + this.status = Status.CANCELLED; + } + + isCancelled() { + return this.status === Status.CANCELLED; + } +} diff --git a/src/styles/_vars.scss b/packages/shared/src/_vars.scss similarity index 61% rename from src/styles/_vars.scss rename to packages/shared/src/_vars.scss index bbf26d9bb..49bc24f4e 100644 --- a/src/styles/_vars.scss +++ b/packages/shared/src/_vars.scss @@ -1,102 +1,109 @@ // *** MAIN *** -$psv-main-background-stops: #fff 0%, #fdfdfd 16%, #fbfbfb 33%, #f8f8f8 49%, #efefef 66%, #dfdfdf 82%, #bfbfbf 100% !default; +$psv-main-background-stops: #fff 0%, + #fdfdfd 16%, + #fbfbfb 33%, + #f8f8f8 49%, + #efefef 66%, + #dfdfdf 82%, + #bfbfbf 100% !default; $psv-main-background: radial-gradient($psv-main-background-stops) !default; $psv-element-focus-outline: 2px solid #007cff !default; - // *** LOADER *** -$psv-loader-color: rgba(61, 61, 61, .7) !default; +$psv-loader-bg-color: rgba(61, 61, 61, 0.5) !default; +$psv-loader-color: rgba(255, 255, 255, 0.7) !default; $psv-loader-width: 150px !default; $psv-loader-tickness: 10px !default; -$psv-loader-font: 14px sans-serif !default; - +$psv-loader-border: 3px !default; +$psv-loader-font: 600 16px sans-serif !default; // *** NAVBAR *** $psv-navbar-height: 40px !default; -$psv-navbar-background: rgba(61, 61, 61, .5) !default; +$psv-navbar-background: rgba(61, 61, 61, 0.5) !default; $psv-caption-font: 16px sans-serif !default; -$psv-caption-color: rgba(255, 255, 255, .7) !default; +$psv-caption-text-color: rgba(255, 255, 255, 0.7) !default; $psv-buttons-height: 20px !default; -$psv-buttons-padding: (($psv-navbar-height - $psv-buttons-height) * .5) !default; +$psv-buttons-padding: (($psv-navbar-height - $psv-buttons-height) * 0.5) !default; $psv-buttons-background: transparent !default; -$psv-buttons-active-background: rgba(255, 255, 255, .2) !default; -$psv-buttons-color: rgba(255, 255, 255, .7) !default; -$psv-buttons-disabled-opacity: .5 !default; +$psv-buttons-active-background: rgba(255, 255, 255, 0.2) !default; +$psv-buttons-color: rgba(255, 255, 255, 0.7) !default; +$psv-buttons-disabled-opacity: 0.5 !default; $psv-buttons-hover-scale: 1.2 !default; $psv-buttons-hover-scale-delay: 200ms !default; $psv-zoom-range-width: 80px !default; $psv-zoom-range-tickness: 1px !default; -$psv-zoom-disk-diameter: 7px !default; +$psv-zoom-range-diameter: 7px !default; $psv-zoom-range-media-min-width: 600px !default; - // *** TOOLTIP *** -$psv-tooltip-background-color: rgba(61, 61, 61, .8) !default; +$psv-tooltip-background: rgba(61, 61, 61, 0.8) !default; $psv-tooltip-animate-offset: 5px !default; $psv-tooltip-animate-delay: 100ms !default; $psv-tooltip-radius: 4px !default; -$psv-tooltip-padding: .5em 1em !default; +$psv-tooltip-padding: 0.5em 1em !default; $psv-tooltip-arrow-size: 7px !default; +$psv-tooltip-arrow-color: $psv-tooltip-background !default; $psv-tooltip-max-width: 200px !default; $psv-tooltip-text-color: rgb(255, 255, 255) !default; $psv-tooltip-font: 14px sans-serif !default; $psv-tooltip-text-shadow: 0 1px #000 !default; -$psv-tooltip-shadow-color: rgba(90, 90, 90, .7) !default; +$psv-tooltip-shadow-color: rgba(90, 90, 90, 0.7) !default; $psv-tooltip-shadow-offset: 3px !default; // the shadow is always at the opposite side of the arrow - // *** PANEL *** -$psv-panel-background: rgba(10, 10, 10, .7) !default; +$psv-panel-background: rgba(10, 10, 10, 0.7) !default; $psv-panel-text-color: rgb(220, 220, 220) !default; $psv-panel-font: 16px sans-serif !default; $psv-panel-width: 400px !default; $psv-panel-padding: 1em !default; +$psv-panel-animate-delay: 100ms !default; $psv-panel-resizer-width: 9px !default; // must be odd -$psv-panel-resizer-background: rgba(0, 0, 0, .9) !default; +$psv-panel-resizer-background: rgba(0, 0, 0, 0.9) !default; $psv-panel-resizer-grip-color: #fff !default; $psv-panel-resizer-grip-height: 29px !default; // must be odd -$psv-panel-close-button-width: 24px !default; +$psv-panel-close-button-size: 32px !default; $psv-panel-close-button-background: $psv-panel-resizer-background !default; $psv-panel-close-button-color: #fff !default; +$psv-panel-close-button-animate-delay: 300ms !default; $psv-panel-title-font: 24px sans-serif !default; $psv-panel-title-icon-size: 24px !default; $psv-panel-title-margin: 24px !default; $psv-panel-menu-item-height: 1.5em !default; -$psv-panel-menu-item-padding: .5em 1em !default; +$psv-panel-menu-item-padding: 0.5em 1em !default; $psv-panel-menu-item-active-outline: 1px !default; -$psv-panel-menu-odd-background: rgba(255, 255, 255, .1) !default; +$psv-panel-menu-odd-background: rgba(255, 255, 255, 0.1) !default; $psv-panel-menu-even-background: transparent !default; -$psv-panel-menu-hover-background: rgba(255, 255, 255, .2) !default; - +$psv-panel-menu-hover-background: rgba(255, 255, 255, 0.2) !default; // *** NOTIFICATION *** $psv-notification-position-from: -$psv-navbar-height !default; $psv-notification-position-to: $psv-navbar-height * 2 !default; $psv-notification-animate-delay: 200ms !default; -$psv-notification-background-color: $psv-tooltip-background-color !default; +$psv-notification-background: $psv-tooltip-background !default; $psv-notification-radius: $psv-tooltip-radius !default; $psv-notification-padding: $psv-tooltip-padding !default; $psv-notification-font: $psv-tooltip-font !default; $psv-notification-text-color: $psv-tooltip-text-color !default; - // *** OVERLAY *** -$psv-overlay-color: black !default; -$psv-overlay-opacity: .8 !default; -$psv-overlay-font-family: sans-serif !default; -$psv-overlay-text-size: 30px !default; -$psv-overlay-subtext-size: 20px !default; -$psv-overlay-image-size: (portrait: 50vw, landscape: 25vw) !default; - +$psv-overlay-opacity: 0.8 !default; +$psv-overlay-title-font: 30px sans-serif !default; +$psv-overlay-title-color: black !default; +$psv-overlay-text-font: 20px sans-serif !default; +$psv-overlay-text-color: rgba(0, 0, 0, 0.8) !default; +$psv-overlay-image-size: ( + portrait: 50vw, + landscape: 25vw, +) !default; // *** Z-INDEXES *** $psv-canvas-zindex: 0 !default; diff --git a/packages/shared/src/autorotate-utils.ts b/packages/shared/src/autorotate-utils.ts new file mode 100644 index 000000000..36b92c9fb --- /dev/null +++ b/packages/shared/src/autorotate-utils.ts @@ -0,0 +1,41 @@ +import type { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin'; + +let debugMarkers: string[] = []; + +/** + * @internal + */ +export function debugCurve(markers: MarkersPlugin, curve: [number, number][], stepSize: number) { + debugMarkers.forEach((marker) => { + try { + markers.removeMarker(marker); + } catch (e) { + // noop + } + }); + + markers.addMarker({ + id: 'autorotate-path', + polyline: curve, + svgStyle: { + stroke: 'white', + strokeWidth: '2px', + }, + }); + debugMarkers = ['autorotate-path']; + + curve.forEach((pos, i) => { + markers.addMarker({ + id: 'autorotate-path-' + i, + circle: 5, + position: { + yaw: pos[0], + pitch: pos[1], + }, + svgStyle: { + fill: i % stepSize === 0 ? 'red' : 'black', + }, + }); + debugMarkers.push('autorotate-path-' + i); + }); +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 000000000..357e7c0ad --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,5 @@ +export * from './AbstractVideoAdapter'; +export * from './autorotate-utils'; +export * from './Queue'; +export * from './Task'; +export * from './tiles-utils'; diff --git a/packages/shared/src/tiles-utils.ts b/packages/shared/src/tiles-utils.ts new file mode 100644 index 000000000..45995a4e2 --- /dev/null +++ b/packages/shared/src/tiles-utils.ts @@ -0,0 +1,59 @@ +import { + BufferGeometry, + CanvasTexture, + LineSegments, + Material, + Mesh, + MeshBasicMaterial, + Object3D, + SphereGeometry, + WireframeGeometry, +} from 'three'; + +/** + * Generates an material for errored tiles + * @internal + */ +export function buildErrorMaterial(width: number, height: number): MeshBasicMaterial { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#333'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.font = `${canvas.width / 5}px serif`; + ctx.fillStyle = '#a22'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('⚠', canvas.width / 2, canvas.height / 2); + + const texture = new CanvasTexture(canvas); + return new MeshBasicMaterial({ map: texture }); +} + +/** + * Creates a wireframe geometry, for debug + * @internal + */ +export function createWireFrame(geometry: BufferGeometry): Object3D { + const wireframe = new WireframeGeometry(geometry); + const line = new LineSegments(wireframe); + line.material.depthTest = false; + line.material.opacity = 0.25; + line.material.transparent = true; + return line; +} + +/** + * Creates a small red sphere, for debug + * @internal + */ +export function createDot(x: number, y: number, z: number) { + const geom = new SphereGeometry(0.1); + const material = new MeshBasicMaterial({ color: 0xff0000 }); + const mesh = new Mesh(geom, material); + mesh.position.set(x, y, z); + return mesh; +} diff --git a/packages/shared/src/umd.spec.ts b/packages/shared/src/umd.spec.ts new file mode 100644 index 000000000..6ca265012 --- /dev/null +++ b/packages/shared/src/umd.spec.ts @@ -0,0 +1,56 @@ +import { execSync } from 'child_process'; +import { readFileSync } from 'fs'; +import assert from 'assert'; +import pkg from '../package.json'; + +/** + * This test ensures that the custom UMD plugin is behaving correctly + */ +describe('UMD', () => { + it('should correctly wrap iife to umd', () => { + execSync('yarn build:dev'); + + const output = readFileSync('./dist/index.js', { encoding: 'utf8' }); + + const expected = `(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('three'), require('@photo-sphere-viewer/core')) : + typeof define === 'function' && define.amd ? define(['exports', 'three', '@photo-sphere-viewer/core'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory((global.PhotoSphereViewer = global.PhotoSphereViewer || {}, global.PhotoSphereViewer.Shared = {}), global.THREE, global.PhotoSphereViewer)); +})(this, (function (exports, THREE, PhotoSphereViewer) { + +/*! + * PhotoSphereViewer.Shared ${pkg.version} + * @copyright ${new Date().getFullYear()} Damien "Mistic" Sorel + * @licence MIT (https://opensource.org/licenses/MIT) + */ +"use strict"; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + + // @photo-sphere-viewer/core + var require_core = () => PhotoSphereViewer; + + // three + var require_three = () => THREE; + + // src/index.ts`; + + const actual = output.split('\n').slice(0, expected.split('\n').length).join('\n'); + + assert.strictEqual(actual, expected); + }).timeout(10000); +}); diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 000000000..34bbb4fa3 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/shared/tsup.config.js b/packages/shared/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/shared/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/shared/typings.d.ts b/packages/shared/typings.d.ts new file mode 100644 index 000000000..bc55a62ca --- /dev/null +++ b/packages/shared/typings.d.ts @@ -0,0 +1,14 @@ +declare module '*.svg' { + const content: string; + export default content; +} + +declare module '*.scss' { + const content: any; + export default content; +} + +declare module '*.json' { + const content: any; + export default content; +} diff --git a/packages/stereo-plugin/.typedoc/README.md b/packages/stereo-plugin/.typedoc/README.md new file mode 100644 index 000000000..42aeec3c3 --- /dev/null +++ b/packages/stereo-plugin/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/stereo-plugin](https://www.npmjs.com/package/@photo-sphere-viewer/stereo-plugin) + +Documentation : https://photo-sphere-viewer.js.org/plugins/stereo diff --git a/packages/stereo-plugin/package.json b/packages/stereo-plugin/package.json new file mode 100644 index 000000000..9337da9f0 --- /dev/null +++ b/packages/stereo-plugin/package.json @@ -0,0 +1,26 @@ +{ + "name": "@photo-sphere-viewer/stereo-plugin", + "version": "0.0.0", + "description": "Photo sphere Viewer plugin to add a stereo view on mobile devices.", + "homepage": "https://photo-sphere-viewer.js.org/plugins/stereo", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0", + "@photo-sphere-viewer/gyroscope-plugin": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.StereoPlugin" + }, + "typedoc": { + "displayName": "plugin: Stereo", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/stereo-plugin/src/StereoButton.ts b/packages/stereo-plugin/src/StereoButton.ts new file mode 100644 index 000000000..eed37ca66 --- /dev/null +++ b/packages/stereo-plugin/src/StereoButton.ts @@ -0,0 +1,53 @@ +import type { Navbar } from '@photo-sphere-viewer/core'; +import { AbstractButton } from '@photo-sphere-viewer/core'; +import { StereoUpdatedEvent } from './events'; +import stereo from './icons/stereo.svg'; +import type { StereoPlugin } from './StereoPlugin'; + +export class StereoButton extends AbstractButton { + static override readonly id = 'stereo'; + static icon = stereo; + + private readonly plugin: StereoPlugin; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-stereo-button', + icon: stereo, + hoverScale: true, + collapsable: true, + tabbable: true, + }); + + this.plugin = this.viewer.getPlugin('stereo'); + + if (this.plugin) { + this.plugin.addEventListener(StereoUpdatedEvent.type, this); + } + } + + override destroy() { + if (this.plugin) { + this.plugin.removeEventListener(StereoUpdatedEvent.type, this); + } + + super.destroy(); + } + + override isSupported() { + return !this.plugin ? false : { initial: false, promise: this.plugin.isSupported }; + } + + handleEvent(e: Event) { + if (e instanceof StereoUpdatedEvent) { + this.toggleActive(e.stereoEnabled); + } + } + + /** + * Toggles stereo control + */ + onClick() { + this.plugin.toggle(); + } +} diff --git a/packages/stereo-plugin/src/StereoEffect.d.ts b/packages/stereo-plugin/src/StereoEffect.d.ts new file mode 100644 index 000000000..8c194760b --- /dev/null +++ b/packages/stereo-plugin/src/StereoEffect.d.ts @@ -0,0 +1,10 @@ +import { Camera, Object3D, Renderer } from 'three'; + +export class StereoEffect { + domElement: HTMLCanvasElement; + + render(scene: Object3D, camera: Camera): void; + setSize(width: number, height: number, updateStyle?: boolean): void; + + constructor(renderer: Renderer); +} diff --git a/packages/stereo-plugin/src/StereoEffect.js b/packages/stereo-plugin/src/StereoEffect.js new file mode 100644 index 000000000..f616489fc --- /dev/null +++ b/packages/stereo-plugin/src/StereoEffect.js @@ -0,0 +1,44 @@ +import { StereoCamera, Vector2 } from 'three'; + +/** + * Copied from three.js examples + * @internal + */ +export class StereoEffect { + constructor(renderer) { + const _stereo = new StereoCamera(); + _stereo.aspect = 0.5; + const size = new Vector2(); + + this.setEyeSeparation = function (eyeSep) { + _stereo.eyeSep = eyeSep; + }; + + this.setSize = function (width, height) { + renderer.setSize(width, height); + }; + + this.render = function (scene, camera) { + scene.updateMatrixWorld(); + + if (camera.parent === null) camera.updateMatrixWorld(); + + _stereo.update(camera); + + renderer.getSize(size); + + if (renderer.autoClear) renderer.clear(); + renderer.setScissorTest(true); + + renderer.setScissor(0, 0, size.width / 2, size.height); + renderer.setViewport(0, 0, size.width / 2, size.height); + renderer.render(scene, _stereo.cameraL); + + renderer.setScissor(size.width / 2, 0, size.width / 2, size.height); + renderer.setViewport(size.width / 2, 0, size.width / 2, size.height); + renderer.render(scene, _stereo.cameraR); + + renderer.setScissorTest(false); + }; + } +} diff --git a/packages/stereo-plugin/src/StereoPlugin.ts b/packages/stereo-plugin/src/StereoPlugin.ts new file mode 100644 index 000000000..e0328fafe --- /dev/null +++ b/packages/stereo-plugin/src/StereoPlugin.ts @@ -0,0 +1,231 @@ +import type { CompassPlugin } from '@photo-sphere-viewer/compass-plugin'; +import type { Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin, events, PSVError, utils } from '@photo-sphere-viewer/core'; +import type { GyroscopePlugin } from '@photo-sphere-viewer/gyroscope-plugin'; +import type { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin'; +import { StereoPluginEvents, StereoUpdatedEvent } from './events'; +import mobileRotateIcon from './icons/mobile-rotate.svg'; +import { StereoEffect } from './StereoEffect'; + +interface WakeLockSentinel { + release(): void; +} + +const ID_OVERLAY_PLEASE_ROTATE = 'pleaseRotate'; + +/** + * Adds stereo view on mobile devices + */ +export class StereoPlugin extends AbstractPlugin { + static override readonly id = 'stereo'; + + private readonly state = { + enabled: false, + wakeLock: null as WakeLockSentinel, + }; + + private gyroscope: GyroscopePlugin; + private markers: MarkersPlugin; + private compass: CompassPlugin; + + /** + * @internal + */ + get isSupported(): Promise { + return this.gyroscope.isSupported(); + } + + constructor(viewer: Viewer) { + super(viewer); + } + + /** + * @internal + */ + override init() { + super.init(); + + this.markers = this.viewer.getPlugin('markers'); + this.compass = this.viewer.getPlugin('compass'); + this.gyroscope = this.viewer.getPlugin('gyroscope'); + + if (!this.gyroscope) { + throw new PSVError('Stereo plugin requires the Gyroscope plugin'); + } + + this.viewer.addEventListener(events.StopAllEvent.type, this); + this.viewer.addEventListener(events.ClickEvent.type, this); + } + + /** + * @internal + */ + override destroy() { + this.viewer.removeEventListener(events.StopAllEvent.type, this); + this.viewer.removeEventListener(events.ClickEvent.type, this); + + this.stop(); + + delete this.markers; + delete this.compass; + delete this.gyroscope; + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof events.StopAllEvent || e instanceof events.ClickEvent) { + this.stop(); + } + } + + /** + * Checks if the stereo view is enabled + */ + isEnabled(): boolean { + return this.state.enabled; + } + + /** + * Enables the stereo view + * @description + * - enables wake lock + * - enables full screen + * - starts gyroscope controle + * - hides markers, navbar and panel + * - instanciate the stereo effect + */ + start(): Promise { + // Need to be in the main event queue + this.viewer.enterFullscreen(); + this.__startWakelock(); + this.__lockOrientation(); + + return this.gyroscope.start().then( + () => { + this.viewer.renderer.setCustomRenderer((renderer) => new StereoEffect(renderer)); + this.state.enabled = true; + + this.markers?.hideAllMarkers(); + this.compass?.hide(); + this.viewer.navbar.hide(); + this.viewer.panel.hide(); + + this.dispatchEvent(new StereoUpdatedEvent(true)); + + this.viewer.notification.show({ + content: this.viewer.config.lang.stereoNotification, + timeout: 3000, + }); + }, + () => { + this.__unlockOrientation(); + this.__stopWakelock(); + this.viewer.exitFullscreen(); + return Promise.reject(); + } + ); + } + + /** + * Disables the stereo view + */ + stop() { + if (this.isEnabled()) { + this.viewer.renderer.setCustomRenderer(null); + this.state.enabled = false; + + this.markers?.showAllMarkers(); + this.compass?.show(); + this.viewer.navbar.show(); + + this.__unlockOrientation(); + this.__stopWakelock(); + this.viewer.exitFullscreen(); + this.gyroscope.stop(); + + this.dispatchEvent(new StereoUpdatedEvent(false)); + } + } + + /** + * Enables or disables the stereo view + */ + toggle() { + if (this.isEnabled()) { + this.stop(); + } else { + this.start(); + } + } + + /** + * Enables WakeLock + */ + private __startWakelock() { + if ('wakeLock' in navigator) { + (navigator as any).wakeLock + .request('screen') + .then((wakeLock: WakeLockSentinel) => { + this.state.wakeLock = wakeLock; + }) + .catch(() => utils.logWarn('Cannot acquire WakeLock')); + } else { + utils.logWarn('WakeLock is not available'); + } + } + + /** + * Disables WakeLock + */ + private __stopWakelock() { + if (this.state.wakeLock) { + this.state.wakeLock.release(); + this.state.wakeLock = null; + } + } + + /** + * Tries to lock the device in landscape or display a message + */ + private __lockOrientation() { + let displayRotateMessageTimeout: ReturnType; + + const displayRotateMessage = () => { + if (window.innerHeight > window.innerWidth) { + this.viewer.overlay.show({ + id: ID_OVERLAY_PLEASE_ROTATE, + image: mobileRotateIcon, + title: this.viewer.config.lang.pleaseRotate, + text: this.viewer.config.lang.tapToContinue, + }); + } + + if (displayRotateMessageTimeout) { + clearTimeout(displayRotateMessageTimeout); + displayRotateMessageTimeout = null; + } + }; + + if (window.screen?.orientation) { + window.screen.orientation.lock('landscape').then(null, () => displayRotateMessage()); + displayRotateMessageTimeout = setTimeout(() => displayRotateMessage(), 500); + } else { + displayRotateMessage(); + } + } + + /** + * Unlock the device orientation + */ + private __unlockOrientation() { + if (window.screen?.orientation) { + window.screen.orientation.unlock(); + } else { + this.viewer.overlay.hide(ID_OVERLAY_PLEASE_ROTATE); + } + } +} diff --git a/packages/stereo-plugin/src/events.ts b/packages/stereo-plugin/src/events.ts new file mode 100644 index 000000000..5c98ef7f9 --- /dev/null +++ b/packages/stereo-plugin/src/events.ts @@ -0,0 +1,15 @@ +import { TypedEvent } from '@photo-sphere-viewer/core'; +import type { StereoPlugin } from './StereoPlugin'; + +/** + * Triggered when the stereo view is enabled/disabled + */ +export class StereoUpdatedEvent extends TypedEvent { + static override readonly type = 'stereo-updated'; + + constructor(public readonly stereoEnabled: boolean) { + super(StereoUpdatedEvent.type); + } +} + +export type StereoPluginEvents = StereoUpdatedEvent; diff --git a/src/plugins/stereo/mobile-rotate.svg b/packages/stereo-plugin/src/icons/mobile-rotate.svg similarity index 94% rename from src/plugins/stereo/mobile-rotate.svg rename to packages/stereo-plugin/src/icons/mobile-rotate.svg index b73b40a3f..362d682ea 100644 --- a/src/plugins/stereo/mobile-rotate.svg +++ b/packages/stereo-plugin/src/icons/mobile-rotate.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/plugins/stereo/stereo.svg b/packages/stereo-plugin/src/icons/stereo.svg similarity index 86% rename from src/plugins/stereo/stereo.svg rename to packages/stereo-plugin/src/icons/stereo.svg index c7fd1ebd2..86adad03c 100644 --- a/src/plugins/stereo/stereo.svg +++ b/packages/stereo-plugin/src/icons/stereo.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/packages/stereo-plugin/src/index.ts b/packages/stereo-plugin/src/index.ts new file mode 100644 index 000000000..a150c4610 --- /dev/null +++ b/packages/stereo-plugin/src/index.ts @@ -0,0 +1,13 @@ +import { DEFAULTS, registerButton } from '@photo-sphere-viewer/core'; +import * as events from './events'; +import { StereoButton } from './StereoButton'; + +DEFAULTS.lang[StereoButton.id] = 'Stereo view'; +registerButton(StereoButton, 'caption:right'); + +DEFAULTS.lang.stereoNotification = 'Tap anywhere to exit stereo view.'; +DEFAULTS.lang.pleaseRotate = 'Please rotate your device'; +DEFAULTS.lang.tapToContinue = '(or tap to continue)'; + +export { StereoPlugin } from './StereoPlugin'; +export { events }; diff --git a/packages/stereo-plugin/tsconfig.json b/packages/stereo-plugin/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/stereo-plugin/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/stereo-plugin/tsup.config.js b/packages/stereo-plugin/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/stereo-plugin/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/video-plugin/.typedoc/README.md b/packages/video-plugin/.typedoc/README.md new file mode 100644 index 000000000..0b2dc7c0a --- /dev/null +++ b/packages/video-plugin/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/video-plugin](https://www.npmjs.com/package/@photo-sphere-viewer/video-plugin) + +Documentation : https://photo-sphere-viewer.js.org/plugins/video diff --git a/packages/video-plugin/package.json b/packages/video-plugin/package.json new file mode 100644 index 000000000..952432fd4 --- /dev/null +++ b/packages/video-plugin/package.json @@ -0,0 +1,29 @@ +{ + "name": "@photo-sphere-viewer/video-plugin", + "version": "0.0.0", + "description": "Photo sphere Viewer plugin to add video controls.", + "homepage": "https://photo-sphere-viewer.js.org/plugins/video", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "optionalDependencies": { + "@photo-sphere-viewer/autorotate-plugin": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix && stylelint \"src/**/*.scss\" --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.VideoPlugin", + "style": true + }, + "typedoc": { + "displayName": "plugin: Video", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/video-plugin/src/VideoPlugin.ts b/packages/video-plugin/src/VideoPlugin.ts new file mode 100644 index 000000000..7ecaf1644 --- /dev/null +++ b/packages/video-plugin/src/VideoPlugin.ts @@ -0,0 +1,421 @@ +import type { AutorotatePlugin } from '@photo-sphere-viewer/autorotate-plugin'; +import type { AbstractAdapter, Position, TextureData, Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin, CONSTANTS, events, PSVError, utils } from '@photo-sphere-viewer/core'; +import type { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin'; +import { SplineCurve, Texture, Vector2 } from 'three'; +import { PauseOverlay } from './components/PauseOverlay'; +import { ProgressBar } from './components/ProgressBar'; +import { BufferEvent, PlayPauseEvent, ProgressEvent, VideoPluginEvents, VolumeChangeEvent } from './events'; +import { VideoKeypoint, VideoPluginConfig } from './model'; +// import { debugCurve } from '@photo-sphere-viewer/shared'; + +const getConfig = utils.getConfigParser({ + progressbar: true, + bigbutton: true, + keypoints: null, +}); + +/** + * Controls a video adapter + */ +export class VideoPlugin extends AbstractPlugin { + static override readonly id = 'video'; + + readonly config: VideoPluginConfig; + + private readonly state = { + curve: null as SplineCurve, + start: null as VideoKeypoint, + end: null as VideoKeypoint, + keypoints: null as VideoKeypoint[], + }; + + private video?: HTMLVideoElement; + private progressbar?: ProgressBar; + private overlay?: PauseOverlay; + + private autorotate?: AutorotatePlugin; + private markers?: MarkersPlugin; + + constructor(viewer: Viewer, config: VideoPluginConfig) { + super(viewer); + + if (!(this.viewer.adapter.constructor as typeof AbstractAdapter).id.includes('video')) { + throw new PSVError('VideoPlugin can only be used with a video adapter.'); + } + + this.config = getConfig(config); + + if (this.config.progressbar) { + this.progressbar = new ProgressBar(this, viewer); + } + + if (this.config.bigbutton) { + this.overlay = new PauseOverlay(this, viewer); + } + } + + /** + * @internal + */ + override init() { + super.init(); + + this.markers = this.viewer.getPlugin('markers'); + this.autorotate = this.viewer.getPlugin('autorotate'); + + if (this.autorotate) { + this.autorotate.config.autostartDelay = 0; + this.autorotate.config.autostartOnIdle = false; + } + + if (this.config.keypoints) { + this.setKeypoints(this.config.keypoints); + delete this.config.keypoints; + } + + this.autorotate?.addEventListener('autorotate', this); + this.viewer.addEventListener(events.BeforeRenderEvent.type, this); + this.viewer.addEventListener(events.PanoramaLoadedEvent.type, this); + this.viewer.addEventListener(events.KeypressEvent.type, this); + } + + /** + * @internal + */ + override destroy() { + this.autorotate?.removeEventListener('autorotate', this); + this.viewer.removeEventListener(events.BeforeRenderEvent.type, this); + this.viewer.removeEventListener(events.PanoramaLoadedEvent.type, this); + this.viewer.removeEventListener(events.KeypressEvent.type, this); + + delete this.progressbar; + delete this.overlay; + delete this.markers; + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + switch (e.type) { + case events.BeforeRenderEvent.type: + this.__autorotate(); + break; + case 'autorotate': + this.__configureAutorotate(); + break; + case events.PanoramaLoadedEvent.type: + this.__bindVideo((e as events.PanoramaLoadedEvent).data); + this.progressbar?.show(); + break; + case events.KeypressEvent.type: + this.__onKeyPress(e as events.KeypressEvent); + break; + case 'play': + case 'pause': + this.dispatchEvent(new PlayPauseEvent(this.isPlaying())); + break; + case 'progress': + this.dispatchEvent(new BufferEvent(this.getBufferProgress())); + break; + case 'volumechange': + this.dispatchEvent(new VolumeChangeEvent(this.getVolume())); + break; + case 'timeupdate': + this.dispatchEvent(new ProgressEvent(this.getTime(), this.getDuration(), this.getProgress())); + break; + } + } + + private __bindVideo(textureData: TextureData) { + if (this.video) { + this.video.removeEventListener('play', this); + this.video.removeEventListener('pause', this); + this.video.removeEventListener('progress', this); + this.video.removeEventListener('volumechange', this); + this.video.removeEventListener('timeupdate', this); + } + + this.video = (textureData as TextureData).texture.image; + + // lib.d.ts is invalid ?? + this.video.addEventListener('play', this as any); + this.video.addEventListener('pause', this as any); + this.video.addEventListener('progress', this as any); + this.video.addEventListener('volumechange', this as any); + this.video.addEventListener('timeupdate', this as any); + } + + private __onKeyPress(e: events.KeypressEvent) { + if (e.key === CONSTANTS.KEY_CODES.Space) { + this.playPause(); + e.preventDefault(); + } + } + + /** + * Returns the durection of the video + */ + getDuration(): number { + return this.video?.duration ?? 0; + } + + /** + * Returns the current time of the video + */ + getTime(): number { + return this.video?.currentTime ?? 0; + } + + /** + * Returns the play progression of the video + */ + getProgress(): number { + return this.video ? this.video.currentTime / this.video.duration : 0; + } + + /** + * Returns if the video is playing + */ + isPlaying(): boolean { + return this.video ? !this.video.paused : false; + } + + /** + * Returns the video volume + */ + getVolume(): number { + return this.video?.muted ? 0 : this.video?.volume ?? 0; + } + + /** + * Starts or pause the video + */ + playPause() { + if (this.video) { + if (this.video.paused) { + this.video.play(); + } else { + this.video.pause(); + } + } + } + + /** + * Starts the video if paused + */ + play() { + if (this.video && this.video.paused) { + this.video.play(); + } + } + + /** + * Pauses the cideo if playing + */ + pause() { + if (this.video && !this.video.paused) { + this.video.pause(); + } + } + + /** + * Sets the volume of the video + */ + setVolume(volume: number) { + if (this.video) { + this.video.muted = false; + this.video.volume = volume; + } + } + + /** + * (Un)mutes the video + * @param [mute] - toggle if undefined + */ + setMute(mute?: boolean) { + if (this.video) { + this.video.muted = mute === undefined ? !this.video.muted : mute; + if (!this.video.muted && this.video.volume === 0) { + this.video.volume = 0.1; + } + } + } + + /** + * Changes the current time of the video + */ + setTime(time: number) { + if (this.video) { + this.video.currentTime = time; + } + } + + /** + * Changes the progression of the video + */ + setProgress(progress: number) { + if (this.video) { + this.video.currentTime = this.video.duration * progress; + } + } + + /** + * @internal + */ + getBufferProgress() { + if (this.video) { + let maxBuffer = 0; + + const buffer = this.video.buffered; + + for (let i = 0, l = buffer.length; i < l; i++) { + if (buffer.start(i) <= this.video.currentTime && buffer.end(i) >= this.video.currentTime) { + maxBuffer = buffer.end(i); + break; + } + } + + return Math.max(this.video.currentTime, maxBuffer) / this.video.duration; + } else { + return 0; + } + } + + /** + * Changes the keypoints + * @throws {@link PSVError} if the configuration is invalid + */ + setKeypoints(keypoints?: VideoKeypoint[]) { + if (!this.autorotate) { + throw new PSVError('Video keypoints required the AutorotatePlugin'); + } + + if (!keypoints) { + this.state.keypoints = null; + this.__configureAutorotate(); + return; + } + + if (keypoints.length < 2) { + throw new PSVError('At least two points are required'); + } + + this.state.keypoints = utils.clone(keypoints); + + if (this.state.keypoints) { + this.state.keypoints.forEach((pt, i) => { + if (pt.position) { + pt.position = this.viewer.dataHelper.cleanPosition(pt.position); + } else { + throw new PSVError(`Keypoint #${i} is missing marker or position`); + } + + if (utils.isNil(pt.time)) { + throw new PSVError(`Keypoint #${i} is missing time`); + } + }); + + this.state.keypoints.sort((a, b) => a.time - b.time); + } + + this.__configureAutorotate(); + } + + private __configureAutorotate() { + delete this.state.curve; + delete this.state.start; + delete this.state.end; + + if (this.autorotate.isEnabled() && this.state.keypoints) { + // cancel core rotation + this.viewer.dynamics.position.stop(); + } + } + + private __autorotate() { + if (!this.autorotate?.isEnabled() || !this.state.keypoints) { + return; + } + + const currentTime = this.getTime(); + const autorotate = this.state; + + if (!autorotate.curve || currentTime < autorotate.start.time || currentTime >= autorotate.end.time) { + this.__autorotateNext(currentTime); + } + + if (autorotate.start === autorotate.end) { + this.viewer.rotate(autorotate.start.position); + } else { + const progress = (currentTime - autorotate.start.time) / (autorotate.end.time - autorotate.start.time); + // only the middle segment contains the current section + const pt = autorotate.curve.getPoint(1 / 3 + progress / 3); + + this.viewer.dynamics.position.goto({ yaw: pt.x, pitch: pt.y }); + } + } + + private __autorotateNext(currentTime: number) { + let k1 = null; + let k2 = null; + + const keypoints = this.state.keypoints; + const l = keypoints.length - 1; + + if (currentTime < keypoints[0].time) { + k1 = 0; + k2 = 0; + } + for (let i = 0; i < l; i++) { + if (currentTime >= keypoints[i].time && currentTime < keypoints[i + 1].time) { + k1 = i; + k2 = i + 1; + break; + } + } + if (currentTime >= keypoints[l].time) { + k1 = l; + k2 = l; + } + + // get the 4 points necessary to compute the current movement + // one point before and two points after the current + const workPoints: Position[] = [ + keypoints[Math.max(0, k1 - 1)].position as Position, + keypoints[k1].position as Position, + keypoints[k2].position as Position, + keypoints[Math.min(l, k2 + 1)].position as Position, + ]; + + // apply offsets to avoid crossing the origin + const workVectors = [new Vector2(workPoints[0].yaw, workPoints[0].pitch)]; + + let k = 0; + for (let i = 1; i <= 3; i++) { + const d = workPoints[i - 1].yaw - workPoints[i].yaw; + if (d > Math.PI) { + // crossed the origin left to right + k += 1; + } else if (d < -Math.PI) { + // crossed the origin right to left + k -= 1; + } + if (k !== 0 && i === 1) { + // do not modify first point, apply the reverse offset the the previous point instead + workVectors[0].x -= k * 2 * Math.PI; + k = 0; + } + workVectors.push(new Vector2(workPoints[i].yaw + k * 2 * Math.PI, workPoints[i].pitch)); + } + + this.state.curve = new SplineCurve(workVectors); + this.state.start = keypoints[k1]; + this.state.end = keypoints[k2]; + + // debugCurve(this.markers, this.autorotate.curve.getPoints(16 * 3).map(p => ([p.x, p.y])), 16); + } +} diff --git a/packages/video-plugin/src/components/PauseOverlay.ts b/packages/video-plugin/src/components/PauseOverlay.ts new file mode 100644 index 000000000..f10c45a0e --- /dev/null +++ b/packages/video-plugin/src/components/PauseOverlay.ts @@ -0,0 +1,43 @@ +import type { Viewer } from '@photo-sphere-viewer/core'; +import { AbstractComponent, events, utils } from '@photo-sphere-viewer/core'; +import { PlayPauseEvent } from '../events'; +import playIcon from '../icons/play.svg'; +import { VideoPlugin } from '../VideoPlugin'; + +export class PauseOverlay extends AbstractComponent { + private readonly button: HTMLElement; + + constructor(private readonly plugin: VideoPlugin, viewer: Viewer) { + super(viewer, { + className: 'psv-video-overlay', + }); + + this.button = document.createElement('button'); + this.button.className = 'psv-video-bigbutton psv--capture-event'; + this.button.innerHTML = playIcon; + this.container.appendChild(this.button); + + this.viewer.addEventListener(events.PanoramaLoadedEvent.type, this); + this.plugin.addEventListener(PlayPauseEvent.type, this); + this.button.addEventListener('click', this); + } + + override destroy() { + this.viewer.removeEventListener(events.PanoramaLoadedEvent.type, this); + this.plugin.removeEventListener(PlayPauseEvent.type, this); + + super.destroy(); + } + + handleEvent(e: Event) { + switch (e.type) { + case events.PanoramaLoadedEvent.type: + case PlayPauseEvent.type: + utils.toggleClass(this.button, 'psv-video-bigbutton--pause', !this.plugin.isPlaying()); + break; + case 'click': + this.plugin.playPause(); + break; + } + } +} diff --git a/packages/video-plugin/src/components/PlayPauseButton.ts b/packages/video-plugin/src/components/PlayPauseButton.ts new file mode 100644 index 000000000..8a78f0013 --- /dev/null +++ b/packages/video-plugin/src/components/PlayPauseButton.ts @@ -0,0 +1,48 @@ +import type { Navbar } from '@photo-sphere-viewer/core'; +import { AbstractButton } from '@photo-sphere-viewer/core'; +import { PlayPauseEvent } from '../events'; +import pauseIcon from '../icons/pause.svg'; +import playIcon from '../icons/play.svg'; +import type { VideoPlugin } from '../VideoPlugin'; + +export class PlayPauseButton extends AbstractButton { + static override readonly id = 'videoPlay'; + static override readonly groupId = 'video'; + + private readonly plugin?: VideoPlugin; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-video-play-button', + hoverScale: true, + collapsable: false, + tabbable: true, + icon: playIcon, + iconActive: pauseIcon, + }); + + this.plugin = this.viewer.getPlugin('video'); + + this.plugin?.addEventListener(PlayPauseEvent.type, this); + } + + override destroy() { + this.plugin?.removeEventListener(PlayPauseEvent.type, this); + + super.destroy(); + } + + override isSupported() { + return !!this.plugin; + } + + handleEvent(e: Event) { + if (e instanceof PlayPauseEvent) { + this.toggleActive(e.playing); + } + } + + onClick() { + this.plugin.playPause(); + } +} diff --git a/packages/video-plugin/src/components/ProgressBar.ts b/packages/video-plugin/src/components/ProgressBar.ts new file mode 100644 index 000000000..7c4159c7b --- /dev/null +++ b/packages/video-plugin/src/components/ProgressBar.ts @@ -0,0 +1,114 @@ +import type { Viewer } from '@photo-sphere-viewer/core'; +import { AbstractComponent, events, Tooltip, utils } from '@photo-sphere-viewer/core'; +import { BufferEvent, ProgressEvent } from '../events'; +import { formatTime } from '../utils'; +import type { VideoPlugin } from '../VideoPlugin'; + +export class ProgressBar extends AbstractComponent { + private readonly bufferElt: HTMLElement; + private readonly progressElt: HTMLElement; + private readonly handleElt: HTMLElement; + + private readonly slider: utils.Slider; + + protected override readonly state = { + visible: true, + req: null as ReturnType, + tooltip: null as Tooltip, + }; + + constructor(private readonly plugin: VideoPlugin, viewer: Viewer) { + super(viewer, { + className: 'psv-video-progressbar', + }); + + this.bufferElt = document.createElement('div'); + this.bufferElt.className = 'psv-video-progressbar__buffer'; + this.container.appendChild(this.bufferElt); + + this.progressElt = document.createElement('div'); + this.progressElt.className = 'psv-video-progressbar__progress'; + this.container.appendChild(this.progressElt); + + this.handleElt = document.createElement('div'); + this.handleElt.className = 'psv-video-progressbar__handle'; + this.container.appendChild(this.handleElt); + + this.slider = new utils.Slider( + this.container, + utils.SliderDirection.HORIZONTAL, + this.__onSliderUpdate.bind(this) + ); + + this.viewer.addEventListener(events.PanoramaLoadedEvent.type, this); + this.plugin.addEventListener(BufferEvent.type, this); + this.plugin.addEventListener(ProgressEvent.type, this); + + this.state.req = window.requestAnimationFrame(() => this.__updateProgress()); + + this.hide(); + } + + override destroy() { + this.viewer.removeEventListener(events.PanoramaLoadedEvent.type, this); + this.plugin.removeEventListener(BufferEvent.type, this); + this.plugin.removeEventListener(ProgressEvent.type, this); + + this.slider.destroy(); + this.state.tooltip?.hide(); + window.cancelAnimationFrame(this.state.req); + + delete this.state.tooltip; + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + switch (e.type) { + case events.PanoramaLoadedEvent.type: + case BufferEvent.type: + case ProgressEvent.type: + this.bufferElt.style.width = `${this.plugin.getBufferProgress() * 100}%`; + break; + } + } + + private __updateProgress() { + this.progressElt.style.width = `${this.plugin.getProgress() * 100}%`; + + this.state.req = window.requestAnimationFrame(() => this.__updateProgress()); + } + + private __onSliderUpdate(data: utils.SliderUpdateData) { + if (data.mouseover) { + this.handleElt.style.display = 'block'; + this.handleElt.style.left = `${data.value * 100}%`; + + const time = formatTime(this.plugin.getDuration() * data.value); + + if (!this.state.tooltip) { + this.state.tooltip = this.viewer.createTooltip({ + top: data.cursor.clientY, + left: data.cursor.clientX, + content: time, + }); + } else { + this.state.tooltip.update(time, { + top: data.cursor.clientY, + left: data.cursor.clientX, + }); + } + } else { + this.handleElt.style.display = 'none'; + + this.state.tooltip?.hide(); + delete this.state.tooltip; + } + if (data.click) { + this.plugin.setProgress(data.value); + } + } +} diff --git a/packages/video-plugin/src/components/TimeCaption.ts b/packages/video-plugin/src/components/TimeCaption.ts new file mode 100644 index 000000000..ac23d0926 --- /dev/null +++ b/packages/video-plugin/src/components/TimeCaption.ts @@ -0,0 +1,59 @@ +import type { Navbar } from '@photo-sphere-viewer/core'; +import { AbstractButton, events } from '@photo-sphere-viewer/core'; +import { ProgressEvent } from '../events'; +import { formatTime } from '../utils'; +import type { VideoPlugin } from '../VideoPlugin'; + +export class TimeCaption extends AbstractButton { + static override readonly id = 'videoTime'; + static override readonly groupId = 'video'; + + private plugin?: VideoPlugin; + + private readonly contentElt: HTMLElement; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-caption psv-video-time', + hoverScale: false, + collapsable: false, + tabbable: false, + }); + + this.contentElt = document.createElement('div'); + this.contentElt.className = 'psv-caption-content'; + this.container.appendChild(this.contentElt); + + this.plugin = this.viewer.getPlugin('video'); + + if (this.plugin) { + this.viewer.addEventListener(events.PanoramaLoadedEvent.type, this); + this.plugin.addEventListener(ProgressEvent.type, this); + } + } + + override destroy() { + if (this.plugin) { + this.viewer.removeEventListener(events.PanoramaLoadedEvent.type, this); + this.plugin.removeEventListener(ProgressEvent.type, this); + } + + delete this.plugin; + + super.destroy(); + } + + handleEvent(e: Event) { + switch (e.type) { + case events.PanoramaLoadedEvent.type: + case ProgressEvent.type: + // prettier-ignore + this.contentElt.innerHTML = `${formatTime(this.plugin.getTime())} / ${formatTime(this.plugin.getDuration())}`; + break; + } + } + + onClick(): void { + // nothing + } +} diff --git a/packages/video-plugin/src/components/VolumeButton.ts b/packages/video-plugin/src/components/VolumeButton.ts new file mode 100644 index 000000000..97ac6d3fa --- /dev/null +++ b/packages/video-plugin/src/components/VolumeButton.ts @@ -0,0 +1,118 @@ +import type { Navbar } from '@photo-sphere-viewer/core'; +import { AbstractButton, events, utils } from '@photo-sphere-viewer/core'; +import { PlayPauseEvent, VolumeChangeEvent } from '../events'; +import volumeIcon from '../icons/volume.svg'; +import type { VideoPlugin } from '../VideoPlugin'; + +export class VolumeButton extends AbstractButton { + static override readonly id = 'videoVolume'; + static override readonly groupId = 'video'; + + private readonly plugin?: VideoPlugin; + + private readonly rangeContainer: HTMLElement; + private readonly range: HTMLElement; + private readonly trackElt: HTMLElement; + private readonly progressElt: HTMLElement; + private readonly handleElt: HTMLElement; + + private readonly slider: utils.Slider; + + constructor(navbar: Navbar) { + super(navbar, { + className: 'psv-video-volume-button', + hoverScale: true, + collapsable: false, + tabbable: true, + icon: volumeIcon, + }); + + this.plugin = this.viewer.getPlugin('video'); + + if (this.plugin) { + this.rangeContainer = document.createElement('div'); + this.rangeContainer.className = 'psv-video-volume__container'; + this.container.appendChild(this.rangeContainer); + + this.range = document.createElement('div'); + this.range.className = 'psv-video-volume__range'; + this.rangeContainer.appendChild(this.range); + + this.trackElt = document.createElement('div'); + this.trackElt.className = 'psv-video-volume__track'; + this.range.appendChild(this.trackElt); + + this.progressElt = document.createElement('div'); + this.progressElt.className = 'psv-video-volume__progress'; + this.range.appendChild(this.progressElt); + + this.handleElt = document.createElement('div'); + this.handleElt.className = 'psv-video-volume__handle'; + this.range.appendChild(this.handleElt); + + this.slider = new utils.Slider( + this.range, + utils.SliderDirection.VERTICAL, + this.__onSliderUpdate.bind(this) + ); + + this.viewer.addEventListener(events.PanoramaLoadedEvent.type, this); + this.plugin.addEventListener(PlayPauseEvent.type, this); + this.plugin.addEventListener(VolumeChangeEvent.type, this); + + this.__setVolume(0); + } + } + + override destroy() { + if (this.plugin) { + this.viewer.removeEventListener(events.PanoramaLoadedEvent.type, this); + this.plugin.removeEventListener(PlayPauseEvent.type, this); + this.plugin.removeEventListener(VolumeChangeEvent.type, this); + } + + this.slider.destroy(); + + super.destroy(); + } + + override isSupported() { + return !!this.plugin; + } + + handleEvent(e: Event) { + switch (e.type) { + case events.PanoramaLoadedEvent.type: + case PlayPauseEvent.type: + case VolumeChangeEvent.type: + this.__setVolume(this.plugin.getVolume()); + break; + } + } + + onClick() { + this.plugin.setMute(); + } + + private __onSliderUpdate(data: utils.SliderUpdateData) { + if (data.mousedown) { + this.plugin.setVolume(data.value); + } + } + + private __setVolume(volume: number) { + let level; + if (volume === 0) level = 0; + else if (volume < 0.333) level = 1; + else if (volume < 0.666) level = 2; + else level = 3; + + utils.toggleClass(this.container, 'psv-video-volume-button--0', level === 0); + utils.toggleClass(this.container, 'psv-video-volume-button--1', level === 1); + utils.toggleClass(this.container, 'psv-video-volume-button--2', level === 2); + utils.toggleClass(this.container, 'psv-video-volume-button--3', level === 3); + + this.handleElt.style.bottom = `${volume * 100}%`; + this.progressElt.style.height = `${volume * 100}%`; + } +} diff --git a/packages/video-plugin/src/events.ts b/packages/video-plugin/src/events.ts new file mode 100644 index 000000000..f4fb353e4 --- /dev/null +++ b/packages/video-plugin/src/events.ts @@ -0,0 +1,49 @@ +import { TypedEvent } from '@photo-sphere-viewer/core'; +import type { VideoPlugin } from './VideoPlugin'; + +/** + * @event Triggered when the video starts playing or is paused + */ +export class PlayPauseEvent extends TypedEvent { + static override readonly type = 'play-pause'; + + constructor(public readonly playing: boolean) { + super(PlayPauseEvent.type); + } +} + +/** + * @event Triggered when the video volume changes + */ +export class VolumeChangeEvent extends TypedEvent { + static override readonly type = 'volume-change'; + + constructor(public readonly volume: number) { + super(VolumeChangeEvent.type); + } +} + +/** + * @event Triggered when the video play progression changes + */ +export class ProgressEvent extends TypedEvent { + static override readonly type = 'progress'; + + constructor(public readonly time: number, public readonly duration: number, public readonly progress: number) { + super(ProgressEvent.type); + } +} + +/** + * @event Triggered when the video buffer changes + * @internal + */ +export class BufferEvent extends TypedEvent { + static override readonly type = 'buffer'; + + constructor(public readonly maxBuffer: number) { + super(BufferEvent.type); + } +} + +export type VideoPluginEvents = PlayPauseEvent | VolumeChangeEvent | ProgressEvent | BufferEvent; diff --git a/src/plugins/video/pause.svg b/packages/video-plugin/src/icons/pause.svg similarity index 90% rename from src/plugins/video/pause.svg rename to packages/video-plugin/src/icons/pause.svg index 22a3936a7..dc857f1ee 100644 --- a/src/plugins/video/pause.svg +++ b/packages/video-plugin/src/icons/pause.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/plugins/video/play.svg b/packages/video-plugin/src/icons/play.svg similarity index 96% rename from src/plugins/video/play.svg rename to packages/video-plugin/src/icons/play.svg index 5eebb16e0..d546f28c1 100644 --- a/src/plugins/video/play.svg +++ b/packages/video-plugin/src/icons/play.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/plugins/video/volume.svg b/packages/video-plugin/src/icons/volume.svg similarity index 96% rename from src/plugins/video/volume.svg rename to packages/video-plugin/src/icons/volume.svg index d9b1861a8..85805ffcf 100644 --- a/src/plugins/video/volume.svg +++ b/packages/video-plugin/src/icons/volume.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/packages/video-plugin/src/index.ts b/packages/video-plugin/src/index.ts new file mode 100644 index 000000000..4568dffcd --- /dev/null +++ b/packages/video-plugin/src/index.ts @@ -0,0 +1,19 @@ +import { DEFAULTS, registerButton } from '@photo-sphere-viewer/core'; +import { PlayPauseButton } from './components/PlayPauseButton'; +import { TimeCaption } from './components/TimeCaption'; +import { VolumeButton } from './components/VolumeButton'; +import * as events from './events'; + +DEFAULTS.lang[PlayPauseButton.id] = 'Play/Pause'; +DEFAULTS.lang[VolumeButton.id] = 'Volume'; +registerButton(PlayPauseButton); +registerButton(VolumeButton); +registerButton(TimeCaption); +DEFAULTS.navbar.unshift(PlayPauseButton.groupId); + +export { VideoPlugin } from './VideoPlugin'; +export * from './model'; +export { events }; + +/** @internal */ +import './style.scss'; diff --git a/packages/video-plugin/src/model.ts b/packages/video-plugin/src/model.ts new file mode 100644 index 000000000..b7e0ad682 --- /dev/null +++ b/packages/video-plugin/src/model.ts @@ -0,0 +1,23 @@ +import { ExtendedPosition } from '@photo-sphere-viewer/core'; + +export type VideoKeypoint = { + position: ExtendedPosition; + time: number; +}; + +export type VideoPluginConfig = { + /** + * displays a progressbar on top of the navbar + * @default true + */ + progressbar?: boolean; + /** + * displays a big "play" button in the center of the viewer + * @default true + */ + bigbutton?: boolean; + /** + * defines autorotate timed keypoints + */ + keypoints?: VideoKeypoint[]; +}; diff --git a/packages/video-plugin/src/style.scss b/packages/video-plugin/src/style.scss new file mode 100644 index 000000000..2a1ccb912 --- /dev/null +++ b/packages/video-plugin/src/style.scss @@ -0,0 +1,203 @@ +@use 'sass:map'; +@import '../../shared/src/vars'; + +$psv-progressbar-height: 3px !default; +$psv-progressbar-height-active: 5px !default; +$psv-progressbar-container: 20px !default; +$psv-progressbar-progress-color: $psv-buttons-color !default; +$psv-progressbar-buffer-color: $psv-buttons-active-background !default; +$psv-progressbar-handle-size: 9px !default; +$psv-progressbar-handle-color: white !default; + +$psv-volume-height: 80px !default; +$psv-volume-width: $psv-progressbar-height-active !default; +$psv-volume-bar-color: $psv-progressbar-progress-color !default; +$psv-volume-track-color: $psv-progressbar-buffer-color !default; +$psv-volume-handle-size: $psv-progressbar-handle-size !default; +$psv-volume-handle-color: $psv-progressbar-handle-color !default; + +$psv-video-bigbutton-size: ( + portrait: 20vw, + landscape: 10vw, +) !default; +$psv-video-bigbutton-color: $psv-buttons-color !default; + +.psv-video { + &-progressbar { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: $psv-progressbar-container; + cursor: pointer; + z-index: $psv-navbar-zindex - 1; + + @at-root .psv--has-navbar & { + bottom: $psv-navbar-height; + } + + & > * { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: $psv-progressbar-height; + transition: height 0.2s linear; + } + + &:hover > * { + height: $psv-progressbar-height-active; + } + + &__progress { + background-color: $psv-progressbar-progress-color; + } + + &__buffer { + background-color: $psv-progressbar-buffer-color; + } + + &__handle { + display: none; + height: $psv-progressbar-handle-size !important; + width: $psv-progressbar-handle-size; + border-radius: 50%; + margin-bottom: #{- ($psv-progressbar-handle-size - $psv-progressbar-height-active) * 0.5}; + margin-left: #{- $psv-progressbar-handle-size * 0.5}; + background: $psv-progressbar-handle-color; + } + } + + &-time { + flex: 0 0 auto; + + .psv-caption-content { + min-width: 6em; + text-align: center; + } + } + + &-volume { + &__container { + position: absolute; + left: 0; + bottom: $psv-navbar-height; + padding: $psv-buttons-height 0; + width: $psv-navbar-height; + height: 0; + opacity: 0; + background: $psv-navbar-background; + transition: opacity 0.2s linear, height 0.3s step-end; + } + + &__range { + position: relative; + height: $psv-volume-height; + } + + &__progress, + &__track { + position: absolute; + bottom: 0; + left: #{($psv-navbar-height - $psv-volume-width) * 0.5}; + width: $psv-volume-width; + background: $psv-volume-bar-color; + } + + &__track { + height: 100%; + background: $psv-volume-track-color; + } + + &__handle { + position: absolute; + left: #{($psv-navbar-height - $psv-volume-width) * 0.5}; + height: $psv-volume-handle-size; + width: $psv-volume-handle-size; + border-radius: 50%; + margin-left: #{- ($psv-volume-handle-size - $psv-volume-width) * 0.5}; + margin-bottom: #{- $psv-volume-handle-size * 0.5}; + background: $psv-volume-handle-color; + } + } + + &-volume-button { + position: relative; + + &:hover .psv-video-volume__container { + height: $psv-volume-height; + opacity: 1; + transition-timing-function: linear, step-start; + transition-delay: 0.2s; + } + + &--0 .psv-button-svg { + #lvl1, + #lvl2, + #lvl3 { + fill: none; + } + } + + &--1 .psv-button-svg { + #lvl0, + #lvl2, + #lvl3 { + fill: none; + } + } + + &--2 .psv-button-svg { + #lvl0, + #lvl3 { + fill: none; + } + } + + &--3 .psv-button-svg { + #lvl0 { + fill: none; + } + } + } + + &-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + z-index: $psv-overlay-zindex; + pointer-events: none; + } + + &-bigbutton { + display: block; + border: none; + background: none; + padding: 0; + color: $psv-video-bigbutton-color; + pointer-events: auto; + cursor: pointer; + opacity: 0; + width: 0; + transition: opacity 0.2s linear, width 0.3s step-end; + + &--pause { + width: map.get($psv-video-bigbutton-size, portrait); + opacity: 1; + transition-timing-function: linear, step-start; + + @media (orientation: landscape) { + width: map.get($psv-video-bigbutton-size, landscape); + } + } + + svg { + width: 100%; + } + } +} diff --git a/packages/video-plugin/src/utils.ts b/packages/video-plugin/src/utils.ts new file mode 100644 index 000000000..377dea770 --- /dev/null +++ b/packages/video-plugin/src/utils.ts @@ -0,0 +1,5 @@ +export function formatTime(time: number) { + const seconds = Math.round(time % 60); + const minutes = Math.round(time - seconds) / 60; + return `${minutes}:${('0' + seconds).slice(-2)}`; +} diff --git a/packages/video-plugin/tsconfig.json b/packages/video-plugin/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/video-plugin/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/video-plugin/tsup.config.js b/packages/video-plugin/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/video-plugin/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/virtual-tour-plugin/.typedoc/README.md b/packages/virtual-tour-plugin/.typedoc/README.md new file mode 100644 index 000000000..33d97b553 --- /dev/null +++ b/packages/virtual-tour-plugin/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/virtual-tour-plugin](https://www.npmjs.com/package/@photo-sphere-viewer/virtual-tour-plugin) + +Documentation : https://photo-sphere-viewer.js.org/plugins/virtual-tour diff --git a/packages/virtual-tour-plugin/package.json b/packages/virtual-tour-plugin/package.json new file mode 100644 index 000000000..92408b62c --- /dev/null +++ b/packages/virtual-tour-plugin/package.json @@ -0,0 +1,31 @@ +{ + "name": "@photo-sphere-viewer/virtual-tour-plugin", + "version": "0.0.0", + "description": "Photo sphere Viewer plugin to create virtual tours by linking multiple panoramas.", + "homepage": "https://photo-sphere-viewer.js.org/plugins/virtual-tour", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "optionalDependencies": { + "@photo-sphere-viewer/compass-plugin": "0.0.0", + "@photo-sphere-viewer/gallery-plugin": "0.0.0", + "@photo-sphere-viewer/markers-plugin": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix && stylelint \"src/**/*.scss\" --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.VirtualTourPlugin", + "style": true + }, + "typedoc": { + "displayName": "plugin: VirtualTour", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/virtual-tour-plugin/src/VirtualTourPlugin.ts b/packages/virtual-tour-plugin/src/VirtualTourPlugin.ts new file mode 100644 index 000000000..85b9e461c --- /dev/null +++ b/packages/virtual-tour-plugin/src/VirtualTourPlugin.ts @@ -0,0 +1,560 @@ +import type { CompassPlugin } from '@photo-sphere-viewer/compass-plugin'; +import type { Point, Position, Tooltip, Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin, CONSTANTS, events, PSVError, utils } from '@photo-sphere-viewer/core'; +import type { GalleryPlugin } from '@photo-sphere-viewer/gallery-plugin'; +import type { events as markersEvents, MarkersPlugin } from '@photo-sphere-viewer/markers-plugin'; +import { + AmbientLight, + BackSide, + Group, + MathUtils, + Mesh, + MeshBasicMaterial, + MeshLambertMaterial, + PointLight, +} from 'three'; +import { ARROW_GEOM, ARROW_OUTLINE_GEOM, DEFAULT_ARROW, DEFAULT_MARKER, LINK_DATA } from './constants'; +import { AbstractDatasource } from './datasources/AbstractDataSource'; +import { ClientSideDatasource } from './datasources/ClientSideDatasource'; +import { ServerSideDatasource } from './datasources/ServerSideDatasource'; +import { NodeChangedEvent, VirtualTourEvents } from './events'; +import { VirtualTourLink, VirtualTourNode, VirtualTourPluginConfig } from './model'; +import { bearing, distance, setMeshColor } from './utils'; + +const getConfig = utils.getConfigParser( + { + dataMode: 'client', + positionMode: 'manual', + renderMode: '3d', + nodes: null, + getNode: null, + startNodeId: null, + preload: false, + rotateSpeed: '20rpm', + transition: CONSTANTS.DEFAULT_TRANSITION, + linksOnCompass: true, + markerStyle: DEFAULT_MARKER, + arrowStyle: DEFAULT_ARROW, + markerPitchOffset: -0.1, + arrowPosition: 'bottom', + }, + { + dataMode(dataMode) { + if (dataMode !== 'client' && dataMode !== 'server') { + throw new PSVError('VirtualTourPlugin: invalid dataMode'); + } + return dataMode; + }, + positionMode(positionMode) { + if (positionMode !== 'gps' && positionMode !== 'manual') { + throw new PSVError('VirtualTourPlugin: invalid positionMode'); + } + return positionMode; + }, + renderMode(renderMode) { + if (renderMode !== '3d' && renderMode !== 'markers') { + throw new PSVError('VirtualTourPlugin: invalid renderMode'); + } + return renderMode; + }, + markerStyle(markerStyle, { defValue }) { + return { ...defValue, ...markerStyle }; + }, + arrowStyle(arrowStyle, { defValue }) { + return { ...defValue, ...arrowStyle }; + }, + } +); + +/** + * Creates virtual tours by linking multiple panoramas + */ +export class VirtualTourPlugin extends AbstractPlugin { + static override readonly id = 'virtual-tour'; + + readonly config: VirtualTourPluginConfig; + + private readonly state = { + currentNode: null as VirtualTourNode, + currentTooltip: null as Tooltip, + loadingNode: null as string, + preload: {} as Record>, + }; + + private datasource: AbstractDatasource; + private arrowsGroup: Group; + + private markers?: MarkersPlugin; + private compass?: CompassPlugin; + private gallery?: GalleryPlugin; + + get is3D(): boolean { + return this.config.renderMode === '3d'; + } + + get isServerSide(): boolean { + return this.config.dataMode === 'server'; + } + + get isGps(): boolean { + return this.config.positionMode === 'gps'; + } + + constructor(viewer: Viewer, config: VirtualTourPluginConfig) { + super(viewer); + + this.config = getConfig(config); + + if (this.is3D) { + this.arrowsGroup = new Group(); + + const localLight = new PointLight(0xffffff, 1, 0); + localLight.position.set(0, this.config.arrowPosition === 'bottom' ? 2 : -2, 0); + this.arrowsGroup.add(localLight); + } + } + + /** + * @internal + */ + override init() { + super.init(); + + this.markers = this.viewer.getPlugin('markers'); + this.compass = this.viewer.getPlugin('compass'); + this.gallery = this.viewer.getPlugin('gallery'); + + if (!this.is3D && !this.markers) { + throw new PSVError('VirtualTour plugin requires the Markers plugin in markers mode.'); + } + + if (this.markers?.config.markers) { + utils.logWarn( + 'No default markers can be configured on Markers plugin when using VirtualTour plugin. ' + + 'Consider defining `markers` on each tour node.' + ); + delete this.markers.config.markers; + } + + this.datasource = this.isServerSide + ? new ServerSideDatasource(this, this.viewer) + : new ClientSideDatasource(this, this.viewer); + + if (this.is3D) { + this.viewer.observeObjects(LINK_DATA); + + this.viewer.addEventListener(events.PositionUpdatedEvent.type, this); + this.viewer.addEventListener(events.ZoomUpdatedEvent.type, this); + this.viewer.addEventListener(events.ClickEvent.type, this); + this.viewer.addEventListener(events.ObjectEnterEvent.type, this); + this.viewer.addEventListener(events.ObjectHoverEvent.type, this); + this.viewer.addEventListener(events.ObjectLeaveEvent.type, this); + this.viewer.addEventListener(events.ReadyEvent.type, this, { once: true }); + } else { + this.markers.addEventListener('select-marker', this); + } + + if (this.isServerSide) { + if (this.config.startNodeId) { + this.setCurrentNode(this.config.startNodeId); + } + } else if (this.config.nodes) { + this.setNodes(this.config.nodes, this.config.startNodeId); + delete this.config.nodes; + } + } + + /** + * @internal + */ + override destroy() { + if (this.markers) { + this.markers.removeEventListener('select-marker', this); + } + if (this.arrowsGroup) { + this.viewer.renderer.removeObject(this.arrowsGroup); + } + + this.viewer.removeEventListener(events.PositionUpdatedEvent.type, this); + this.viewer.removeEventListener(events.ZoomUpdatedEvent.type, this); + this.viewer.removeEventListener(events.ClickEvent.type, this); + this.viewer.removeEventListener(events.ObjectEnterEvent.type, this); + this.viewer.removeEventListener(events.ObjectHoverEvent.type, this); + this.viewer.removeEventListener(events.ObjectLeaveEvent.type, this); + this.viewer.removeEventListener(events.ReadyEvent.type, this); + + this.viewer.unobserveObjects(LINK_DATA); + + this.datasource.destroy(); + + delete this.datasource; + delete this.markers; + delete this.compass; + delete this.gallery; + delete this.arrowsGroup; + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + if (e instanceof events.ReadyEvent) { + this.__positionArrows(); + this.viewer.renderer.addObject(this.arrowsGroup); + + const ambientLight = new AmbientLight(0xffffff, 1); + this.viewer.renderer.addObject(ambientLight); + + this.viewer.needsUpdate(); + } else if (e instanceof events.PositionUpdatedEvent || e instanceof events.ZoomUpdatedEvent) { + this.__positionArrows(); + } else if (e instanceof events.ClickEvent) { + const link = e.data.objects.find((o) => o.userData[LINK_DATA])?.userData[LINK_DATA]; + if (link) { + this.setCurrentNode(link.nodeId, link); + } + } else if (e.type === 'select-marker') { + const link = (e as markersEvents.SelectMarkerEvent).marker.data?.[LINK_DATA]; + if (link) { + this.setCurrentNode(link.nodeId, link); + } + } else if (e instanceof events.ObjectEnterEvent) { + if (e.userDataKey === LINK_DATA) { + this.__onEnterObject(e.object, e.viewerPoint); + } + } else if (e instanceof events.ObjectLeaveEvent) { + if (e.userDataKey === LINK_DATA) { + this.__onLeaveObject(e.object); + } + } else if (e instanceof events.ObjectHoverEvent) { + if (e.userDataKey === LINK_DATA) { + this.__onHoverObject(e.viewerPoint); + } + } + } + + /** + * Sets the nodes (client mode only) + * @throws {@link PSVError} if not in client mode + */ + setNodes(nodes: VirtualTourNode[], startNodeId?: string) { + if (this.isServerSide) { + throw new PSVError('Cannot set nodes in server side mode'); + } + + (this.datasource as ClientSideDatasource).setNodes(nodes); + + if (!startNodeId) { + startNodeId = nodes[0].id; + } else if (!this.datasource.nodes[startNodeId]) { + startNodeId = nodes[0].id; + utils.logWarn(`startNodeId not found is provided nodes, resetted to ${startNodeId}`); + } + + this.setCurrentNode(startNodeId); + + if (this.gallery) { + this.gallery.setItems( + nodes.map((node) => ({ + id: node.id, + panorama: node.panorama, + name: node.name, + thumbnail: node.thumbnail, + options: { + caption: node.caption, + panoData: node.panoData, + sphereCorrection: node.sphereCorrection, + description: node.description, + }, + })), + (id) => { + this.setCurrentNode(id as string); + this.gallery.hide(); + } + ); + } + } + + /** + * Changes the current node + * @returns {Promise} resolves false if the loading was aborted by another call + */ + setCurrentNode(nodeId: string, fromLink?: VirtualTourLink): Promise { + if (nodeId === this.state.currentNode?.id) { + return Promise.resolve(true); + } + + this.viewer.hideError(); + + this.state.loadingNode = nodeId; + + const fromNode = this.state.currentNode; + const fromLinkPosition = fromNode && fromLink ? this.__getLinkPosition(fromNode, fromLink) : null; + + return Promise.all([ + // if this node is already preloading, wait for it + Promise.resolve(this.state.preload[nodeId]).then(() => { + if (this.state.loadingNode !== nodeId) { + throw utils.getAbortError(); + } + + return this.datasource.loadNode(nodeId); + }), + Promise.resolve(fromLinkPosition ? this.config.rotateSpeed : false) + .then((speed) => { + if (speed) { + return this.viewer.animate({ ...fromLinkPosition, speed }); + } + }) + .then(() => { + this.viewer.loader.show(); + }), + ]) + .then(([node]) => { + if (this.state.loadingNode !== nodeId) { + throw utils.getAbortError(); + } + + this.state.currentNode = node; + + if (this.state.currentTooltip) { + this.state.currentTooltip.hide(); + this.state.currentTooltip = null; + } + + if (this.is3D) { + this.arrowsGroup.remove(...this.arrowsGroup.children.filter((o) => (o as Mesh).isMesh)); + } + + this.markers?.clearMarkers(); + this.compass?.clearHotspots(); + + return this.viewer + .setPanorama(node.panorama, { + transition: this.config.transition, + caption: node.caption, + description: node.description, + panoData: node.panoData, + sphereCorrection: node.sphereCorrection, + }) + .then((completed) => { + if (!completed) { + throw utils.getAbortError(); + } + }); + }) + .then(() => { + if (this.state.loadingNode !== nodeId) { + throw utils.getAbortError(); + } + + const node = this.state.currentNode; + + if (node.markers) { + if (this.markers) { + this.markers.setMarkers(node.markers); + } else { + utils.logWarn(`Node ${node.id} markers ignored because the plugin is not loaded.`); + } + } + + this.__renderLinks(node); + this.__preload(node); + + this.dispatchEvent( + new NodeChangedEvent(node, { + fromNode, + fromLink, + fromLinkPosition, + }) + ); + + this.state.loadingNode = null; + + return true; + }) + .catch((err) => { + if (utils.isAbortError(err)) { + return false; + } + + this.viewer.showError(this.viewer.config.lang.loadError); + + this.viewer.loader.hide(); + this.viewer.navbar.setCaption(''); + + this.state.loadingNode = null; + + throw err; + }); + } + + /** + * Adds the links for the node + */ + private __renderLinks(node: VirtualTourNode) { + const positions: Position[] = []; + + node.links.forEach((link) => { + const position = this.__getLinkPosition(node, link); + positions.push(position); + + if (this.is3D) { + const mesh = new Mesh(ARROW_GEOM, new MeshLambertMaterial()); + mesh.userData = { [LINK_DATA]: link }; + mesh.rotation.order = 'YXZ'; + mesh.rotateY(-position.yaw); + this.viewer.dataHelper + .sphericalCoordsToVector3({ yaw: position.yaw, pitch: 0 }, mesh.position) + .multiplyScalar(1 / CONSTANTS.SPHERE_RADIUS); + + const outlineMesh = new Mesh(ARROW_OUTLINE_GEOM, new MeshBasicMaterial({ side: BackSide })); + outlineMesh.position.copy(mesh.position); + outlineMesh.rotation.copy(mesh.rotation); + + setMeshColor(mesh, link.arrowStyle?.color || this.config.arrowStyle.color); + setMeshColor(outlineMesh, link.arrowStyle?.outlineColor || this.config.arrowStyle.outlineColor); + + this.arrowsGroup.add(mesh); + this.arrowsGroup.add(outlineMesh); + } else { + if (this.isGps) { + position.pitch += this.config.markerPitchOffset; + } + + this.markers.addMarker( + { + ...this.config.markerStyle, + ...link.markerStyle, + position: position, + id: `tour-link-${link.nodeId}`, + tooltip: link.name, + visible: true, + hideList: true, + data: { [LINK_DATA]: link }, + }, + false + ); + } + }); + + if (this.is3D) { + this.__positionArrows(); + } else { + this.markers.renderMarkers(); + } + + if (this.config.linksOnCompass && this.compass) { + this.compass.setHotspots(positions); + } + } + + /** + * Computes the marker position for a link + */ + private __getLinkPosition(node: VirtualTourNode, link: VirtualTourLink): Position { + if (this.isGps) { + const p1: [number, number] = [MathUtils.degToRad(node.gps[0]), MathUtils.degToRad(node.gps[1])]; + const p2: [number, number] = [MathUtils.degToRad(link.gps[0]), MathUtils.degToRad(link.gps[1])]; + const h1 = node.gps[2] !== undefined ? node.gps[2] : link.gps[2] || 0; + const h2 = link.gps[2] !== undefined ? link.gps[2] : node.gps[2] || 0; + + let pitch = 0; + if (h1 !== h2) { + pitch = Math.atan((h2 - h1) / distance(p1, p2)); + } + + const yaw = bearing(p1, p2); + + return { yaw, pitch }; + } else { + return this.viewer.dataHelper.cleanPosition(link.position); + } + } + + private __onEnterObject(mesh: Mesh, viewerPoint: Point) { + const link = mesh.userData[LINK_DATA]; + + setMeshColor(mesh as any, link.arrowStyle?.hoverColor || this.config.arrowStyle.hoverColor); + + if (link.name) { + this.state.currentTooltip = this.viewer.createTooltip({ + left: viewerPoint.x, + top: viewerPoint.y, + content: link.name, + }); + } + + this.viewer.needsUpdate(); + } + + private __onHoverObject(viewerPoint: Point) { + if (this.state.currentTooltip) { + this.state.currentTooltip.move({ + left: viewerPoint.x, + top: viewerPoint.y, + }); + } + } + + private __onLeaveObject(mesh: Mesh) { + const link = mesh.userData[LINK_DATA]; + + setMeshColor(mesh as any, link.arrowStyle?.color || this.config.arrowStyle.color); + + if (this.state.currentTooltip) { + this.state.currentTooltip.hide(); + this.state.currentTooltip = null; + } + + this.viewer.needsUpdate(); + } + + /** + * Updates to position of the group of arrows + */ + private __positionArrows() { + this.arrowsGroup.position.copy(this.viewer.state.direction).multiplyScalar(0.5); + const s = this.config.arrowStyle.scale; + const f = s[1] + (s[0] - s[1]) * (this.viewer.getZoomLevel() / 100); + const y = 2.5 - (this.viewer.getZoomLevel() / 100) * 1.5; + this.arrowsGroup.position.y += this.config.arrowPosition === 'bottom' ? -y : y; + this.arrowsGroup.scale.set(f, f, f); + } + + /** + * Manage the preload of the linked panoramas + */ + private __preload(node: VirtualTourNode) { + if (!this.config.preload) { + return; + } + + this.state.preload[node.id] = true; + + this.state.currentNode.links + .filter((link) => !this.state.preload[link.nodeId]) + .filter((link) => { + if (typeof this.config.preload === 'function') { + return this.config.preload(this.state.currentNode, link); + } else { + return true; + } + }) + .forEach((link) => { + this.state.preload[link.nodeId] = this.datasource + .loadNode(link.nodeId) + .then((linkNode) => { + return this.viewer.textureLoader.preloadPanorama(linkNode.panorama); + }) + .then(() => { + this.state.preload[link.nodeId] = true; + }) + .catch(() => { + delete this.state.preload[link.nodeId]; + }); + }); + } +} diff --git a/src/plugins/virtual-tour/arrow.svg b/packages/virtual-tour-plugin/src/arrow.svg similarity index 100% rename from src/plugins/virtual-tour/arrow.svg rename to packages/virtual-tour-plugin/src/arrow.svg diff --git a/packages/virtual-tour-plugin/src/constants.ts b/packages/virtual-tour-plugin/src/constants.ts new file mode 100644 index 000000000..f4ded6d0f --- /dev/null +++ b/packages/virtual-tour-plugin/src/constants.ts @@ -0,0 +1,48 @@ +import { ObjectLoader } from 'three'; +import arrowIconSvg from './arrow.svg'; +import { VirtualTourArrowStyle, VirtualTourMarkerStyle } from './model'; +import arrowGeometryJson from './models/arrow.json'; +import arrowOutlineGeometryJson from './models/arrow_outline.json'; + +/** + * Property name added to markers and THREE objects + */ +export const LINK_DATA = 'tourLink'; + +/** + * Default style of the link marker + */ +export const DEFAULT_MARKER: VirtualTourMarkerStyle = { + html: arrowIconSvg, + size: { width: 80, height: 80 }, + scale: [0.5, 2], + anchor: 'top center', + className: 'psv-virtual-tour__marker', + style: { + color: 'rgba(0, 208, 255, 0.8)', + }, +}; + +/** + * Default style of the link arrow + */ +export const DEFAULT_ARROW: VirtualTourArrowStyle = { + color: '#aaaaaa', + hoverColor: '#aa5500', + outlineColor: '#000000', + scale: [0.5, 2], +}; + +export const { ARROW_GEOM, ARROW_OUTLINE_GEOM } = (() => { + const loader = new ObjectLoader(); + const geometries = loader.parseGeometries([arrowGeometryJson, arrowOutlineGeometryJson]); + const arrow = geometries[arrowGeometryJson.uuid]; + const arrowOutline = geometries[arrowOutlineGeometryJson.uuid]; + const scale = 0.015; + const rot = Math.PI / 2; + arrow.scale(scale, scale, scale); + arrow.rotateX(rot); + arrowOutline.scale(scale, scale, scale); + arrowOutline.rotateX(rot); + return { ARROW_GEOM: arrow, ARROW_OUTLINE_GEOM: arrowOutline }; +})(); diff --git a/packages/virtual-tour-plugin/src/datasources/AbstractDataSource.ts b/packages/virtual-tour-plugin/src/datasources/AbstractDataSource.ts new file mode 100644 index 000000000..bde8bd774 --- /dev/null +++ b/packages/virtual-tour-plugin/src/datasources/AbstractDataSource.ts @@ -0,0 +1,64 @@ +import type { Viewer } from '@photo-sphere-viewer/core'; +import { PSVError, utils } from '@photo-sphere-viewer/core'; +import { VirtualTourLink, VirtualTourNode } from '../model'; +import type { VirtualTourPlugin } from '../VirtualTourPlugin'; + +export abstract class AbstractDatasource { + nodes: Record = {}; + + constructor(protected readonly plugin: VirtualTourPlugin, protected readonly viewer: Viewer) {} + + // eslint-disable-next-line @typescript-eslint/no-empty-function + destroy() {} + + /** + * @summary Loads a node + * @param {string} nodeId + * @return {Promise} + */ + abstract loadNode(nodeId: string): Promise; + + /** + * Checks the configuration of a node + */ + protected checkNode(node: VirtualTourNode) { + if (!node.id) { + throw new PSVError('No id given for node'); + } + if (!node.panorama) { + throw new PSVError(`No panorama provided for node ${node.id}`); + } + if ('position' in node) { + utils.logWarn('Use the "gps" property to configure the GPS position of a virtual node'); + // @ts-ignore + node.gps = node['position']; + } + if (this.plugin.isGps && !(node.gps?.length >= 2)) { + throw new PSVError(`No position provided for node ${node.id}`); + } + } + + /** + * Checks the configuration of a link + */ + protected checkLink(node: VirtualTourNode, link: VirtualTourLink) { + if (!link.nodeId) { + throw new PSVError(`Link of node ${node.id} has no target id`); + } + if (Array.isArray(link.position)) { + utils.logWarn('Use the "gps" property to configure the GPS position of a virtual link'); + link.gps = link.position as any; + delete link.position; + } + if (utils.isExtendedPosition(link)) { + utils.logWarn('Use the "position" property to configure the position of a virtual link'); + link.position = this.viewer.dataHelper.cleanPosition(link); + } + if (!this.plugin.isGps && !utils.isExtendedPosition(link.position)) { + throw new PSVError(`No position provided for link ${link.nodeId} of node ${node.id}`); + } + if (this.plugin.isGps && !link.gps) { + throw new PSVError(`No GPS position provided for link ${link.nodeId} of node ${node.id}`); + } + } +} diff --git a/packages/virtual-tour-plugin/src/datasources/ClientSideDatasource.ts b/packages/virtual-tour-plugin/src/datasources/ClientSideDatasource.ts new file mode 100644 index 000000000..5871668f8 --- /dev/null +++ b/packages/virtual-tour-plugin/src/datasources/ClientSideDatasource.ts @@ -0,0 +1,60 @@ +import { PSVError, utils } from '@photo-sphere-viewer/core'; +import { VirtualTourNode } from '../model'; +import { AbstractDatasource } from './AbstractDataSource'; + +export class ClientSideDatasource extends AbstractDatasource { + loadNode(nodeId: string) { + if (this.nodes[nodeId]) { + return Promise.resolve(this.nodes[nodeId]); + } else { + return Promise.reject(new PSVError(`Node ${nodeId} not found`)); + } + } + + setNodes(rawNodes: VirtualTourNode[]) { + if (!rawNodes?.length) { + throw new PSVError('No nodes provided'); + } + + const nodes: Record = {}; + const linkedNodes: Record = {}; + + rawNodes.forEach((node) => { + this.checkNode(node); + + if (nodes[node.id]) { + throw new PSVError(`Duplicate node ${node.id}`); + } + if (!node.links) { + utils.logWarn(`Node ${node.id} has no links`); + node.links = []; + } + + nodes[node.id] = node; + }); + + rawNodes.forEach((node) => { + node.links.forEach((link) => { + if (!nodes[link.nodeId]) { + throw new PSVError(`Target node ${link.nodeId} of node ${node.id} does not exists`); + } + + // copy essential data + link.gps = link.gps || nodes[link.nodeId].gps; + link.name = link.name || nodes[link.nodeId].name; + + this.checkLink(node, link); + + linkedNodes[link.nodeId] = true; + }); + }); + + rawNodes.forEach((node) => { + if (!linkedNodes[node.id]) { + utils.logWarn(`Node ${node.id} is never linked to`); + } + }); + + this.nodes = nodes; + } +} diff --git a/packages/virtual-tour-plugin/src/datasources/ServerSideDatasource.ts b/packages/virtual-tour-plugin/src/datasources/ServerSideDatasource.ts new file mode 100644 index 000000000..63254924a --- /dev/null +++ b/packages/virtual-tour-plugin/src/datasources/ServerSideDatasource.ts @@ -0,0 +1,46 @@ +import type { Viewer } from '@photo-sphere-viewer/core'; +import { PSVError, utils } from '@photo-sphere-viewer/core'; +import { VirtualTourPluginConfig } from '../model'; +import { VirtualTourPlugin } from '../VirtualTourPlugin'; +import { AbstractDatasource } from './AbstractDataSource'; + +export class ServerSideDatasource extends AbstractDatasource { + private readonly nodeResolver: VirtualTourPluginConfig['getNode']; + + constructor(plugin: VirtualTourPlugin, viewer: Viewer) { + super(plugin, viewer); + + if (!plugin.config.getNode) { + throw new PSVError('Missing getNode() option.'); + } + + this.nodeResolver = plugin.config.getNode; + } + + loadNode(nodeId: string) { + if (this.nodes[nodeId]) { + return Promise.resolve(this.nodes[nodeId]); + } else { + return Promise.resolve(this.nodeResolver(nodeId)).then((node) => { + this.checkNode(node); + if (!node.links) { + utils.logWarn(`Node ${node.id} has no links`); + node.links = []; + } + + node.links.forEach((link) => { + // copy essential data + if (this.nodes[link.nodeId]) { + link.gps = link.gps || this.nodes[link.nodeId].gps; + link.name = link.name || this.nodes[link.nodeId].name; + } + + this.checkLink(node, link); + }); + + this.nodes[nodeId] = node; + return node; + }); + } + } +} diff --git a/packages/virtual-tour-plugin/src/events.ts b/packages/virtual-tour-plugin/src/events.ts new file mode 100644 index 000000000..ca5b6d38d --- /dev/null +++ b/packages/virtual-tour-plugin/src/events.ts @@ -0,0 +1,23 @@ +import { Position, TypedEvent } from '@photo-sphere-viewer/core'; +import { VirtualTourLink, VirtualTourNode } from './model'; +import type { VirtualTourPlugin } from './VirtualTourPlugin'; + +/** + * @event Triggered when the current node changes + */ +export class NodeChangedEvent extends TypedEvent { + static override readonly type = 'node-changed'; + + constructor( + public readonly node: VirtualTourNode, + public readonly data: { + fromNode: VirtualTourNode; + fromLink: VirtualTourLink; + fromLinkPosition: Position; + } + ) { + super(NodeChangedEvent.type); + } +} + +export type VirtualTourEvents = NodeChangedEvent; diff --git a/packages/virtual-tour-plugin/src/index.ts b/packages/virtual-tour-plugin/src/index.ts new file mode 100644 index 000000000..207c046f4 --- /dev/null +++ b/packages/virtual-tour-plugin/src/index.ts @@ -0,0 +1,8 @@ +import * as events from './events'; + +export * from './model'; +export { VirtualTourPlugin } from './VirtualTourPlugin'; +export { events }; + +/** @internal */ +import './style.scss'; diff --git a/packages/virtual-tour-plugin/src/model.ts b/packages/virtual-tour-plugin/src/model.ts new file mode 100644 index 000000000..28a3510b3 --- /dev/null +++ b/packages/virtual-tour-plugin/src/model.ts @@ -0,0 +1,183 @@ +import type { ExtendedPosition, PanoData, PanoDataProvider, SphereCorrection } from '@photo-sphere-viewer/core'; +import type { MarkerConfig } from '@photo-sphere-viewer/markers-plugin'; + +/** + * Style of the arrow in 3D mode + */ +export type VirtualTourArrowStyle = { + /** + * @default '#aaaaaa' + */ + color?: string; + /** + * @default '#aa5500' + */ + hoverColor?: string; + /** + * @default '#000000' + */ + outlineColor?: string; + /** + * @default [0.5,2] + */ + scale?: [number, number]; +}; + +/** + * Style of the marker in markers mode + */ +export type VirtualTourMarkerStyle = Omit< + MarkerConfig, + | 'id' + | 'position' + | 'polygonPx' + | 'polygonRad' + | 'polylinePx' + | 'polylineRad' + | 'tooltip' + | 'content' + | 'hideList' + | 'visible' + | 'data' +>; + +/** + * Definition of a link between two nodes + */ +export type VirtualTourLink = Partial & { + /** + * identifier of the target node + */ + nodeId: string; + /** + * override the name of the node (tooltip) + */ + name?: string; + /** + * define the position of the link (manual mode) + */ + position?: ExtendedPosition; + /** + * override the GPS position of the node (GPS mode) + */ + gps?: [number, number, number?]; + /** + * override global marker style + */ + markerStyle?: VirtualTourMarkerStyle; + /** + * override global arrow style + */ + arrowStyle?: VirtualTourArrowStyle; +}; + +/** + * Definition of a single node in the tour + */ +export type VirtualTourNode = { + id: string; + panorama: any; + /** + * short name of the node (links tooltip, gallery) + */ + name?: string; + /** + * caption visible in the navbar + */ + caption?: string; + /** + * description visible in the side panel + */ + description?: string; + /** + * data used for this panorama + */ + panoData?: PanoData | PanoDataProvider; + /** + * sphere correction to apply to this panorama + */ + sphereCorrection?: SphereCorrection; + /** + * links to other nodes + */ + links?: VirtualTourLink[]; + /** + * GPS position (longitude, latitude, optional altitude) + */ + gps?: [number, number, number?]; + /** + * thumbnail for the gallery + */ + thumbnail?: string; + /** + * additional markers to use on this node + */ + markers?: MarkerConfig[]; +}; + +export type VirtualTourPluginConfig = { + /** + * configure data mode + * @default 'client' + */ + dataMode?: 'client' | 'server'; + /** + * configure positioning mode + * @default 'manual' + */ + positionMode?: 'manual' | 'gps'; + /** + * configure rendering mode of links + * @defaul '3d' + */ + renderMode?: '3d' | 'markers'; + /** + * initial nodes (client mode) + */ + nodes?: VirtualTourNode[]; + /** + * function to fetch a node (server mode) + */ + getNode?: (nodeId: string) => VirtualTourNode | Promise; + /** + * id of the initial node, if not defined the first node will be used + */ + startNodeId?: string; + /** + * preload linked panoramas + */ + preload?: boolean | ((node: VirtualTourNode, link: VirtualTourLink) => boolean); + /** + * speed of rotation when clicking on a link, if 'false' the viewer won't rotate at all + * @defaul '20rpm' + */ + rotateSpeed?: false | string | number; + /** + * duration of the transition between nodes + * @default 1500 + */ + transition?: boolean | number; + /** + * if the Compass plugin is enabled, displays the links on the compass, defaults to `true` on in markers render mode + * @default true + */ + linksOnCompass?: boolean; + /** + * global marker style + */ + markerStyle?: VirtualTourMarkerStyle; + /** + * global arrow style + */ + arrowStyle?: VirtualTourArrowStyle; + /** + * (GPS & Markers mode) vertical offset applied to link markers, to compensate for viewer height + * @default -0.1 + */ + markerPitchOffset?: number; + /** + * (3D mode) arrows vertical position + * @default 'bottom' + */ + arrowPosition?: 'top' | 'bottom'; +}; diff --git a/src/plugins/virtual-tour/arrow.json b/packages/virtual-tour-plugin/src/models/arrow.json similarity index 100% rename from src/plugins/virtual-tour/arrow.json rename to packages/virtual-tour-plugin/src/models/arrow.json diff --git a/src/plugins/virtual-tour/arrow_outline.json b/packages/virtual-tour-plugin/src/models/arrow_outline.json similarity index 100% rename from src/plugins/virtual-tour/arrow_outline.json rename to packages/virtual-tour-plugin/src/models/arrow_outline.json diff --git a/src/plugins/virtual-tour/src/arrow.stl b/packages/virtual-tour-plugin/src/models/src/arrow.stl similarity index 100% rename from src/plugins/virtual-tour/src/arrow.stl rename to packages/virtual-tour-plugin/src/models/src/arrow.stl diff --git a/src/plugins/virtual-tour/src/arrow_outline.stl b/packages/virtual-tour-plugin/src/models/src/arrow_outline.stl similarity index 100% rename from src/plugins/virtual-tour/src/arrow_outline.stl rename to packages/virtual-tour-plugin/src/models/src/arrow_outline.stl diff --git a/packages/virtual-tour-plugin/src/style.scss b/packages/virtual-tour-plugin/src/style.scss new file mode 100644 index 000000000..17d11aa7d --- /dev/null +++ b/packages/virtual-tour-plugin/src/style.scss @@ -0,0 +1,26 @@ +@import '../../shared/src/vars'; + +.psv-virtual-tour { + &__marker { + svg { + filter: drop-shadow(0 10px 5px rgba(0, 0, 0, 0.8)); + transform: perspective(100px) rotate3d(1, 0, 0, 0deg); + transform-origin: bottom center; + transition: 0.2s all ease-in-out; + } + + &:hover { + svg { + filter: drop-shadow(0 5px 3px rgba(0, 0, 0, 1)); + transform: perspective(100px) rotate3d(1, 0, 0, 10deg); + } + } + } + + &__menu { + .psv-panel-menu-item-icon { + width: 60px; + height: 60px; + } + } +} diff --git a/packages/virtual-tour-plugin/src/utils.ts b/packages/virtual-tour-plugin/src/utils.ts new file mode 100644 index 000000000..c24ff4685 --- /dev/null +++ b/packages/virtual-tour-plugin/src/utils.ts @@ -0,0 +1,29 @@ +import { utils } from '@photo-sphere-viewer/core'; +import type { Mesh, BufferGeometry, MeshBasicMaterial } from 'three'; + +/** + * Changes the color of a mesh + */ +export function setMeshColor(mesh: Mesh, color: string) { + mesh.material.color.set(color); +} + +/** + * Returns the distance between two GPS points + */ +export function distance(p1: [number, number], p2: [number, number]): number { + return utils.greatArcDistance(p1, p2) * 6371e3; +} + +/** + * Returns the bearing between two GPS points + * @link http://www.movable-type.co.uk/scripts/latlong.html + */ +export function bearing(p1: [number, number], p2: [number, number]): number { + const [long1, lat1] = p1; + const [long2, lat2] = p2; + + const y = Math.sin(long2 - long1) * Math.cos(lat2); + const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(long2 - long1); + return Math.atan2(y, x); +} diff --git a/packages/virtual-tour-plugin/tsconfig.json b/packages/virtual-tour-plugin/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/virtual-tour-plugin/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/virtual-tour-plugin/tsup.config.js b/packages/virtual-tour-plugin/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/virtual-tour-plugin/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/packages/visible-range-plugin/.typedoc/README.md b/packages/visible-range-plugin/.typedoc/README.md new file mode 100644 index 000000000..ec72f178f --- /dev/null +++ b/packages/visible-range-plugin/.typedoc/README.md @@ -0,0 +1,3 @@ +NPM package : [@photo-sphere-viewer/visible-range-plugin](https://www.npmjs.com/package/@photo-sphere-viewer/visible-range-plugin) + +Documentation : https://photo-sphere-viewer.js.org/plugins/visible-range diff --git a/packages/visible-range-plugin/package.json b/packages/visible-range-plugin/package.json new file mode 100644 index 000000000..88b98330d --- /dev/null +++ b/packages/visible-range-plugin/package.json @@ -0,0 +1,25 @@ +{ + "name": "@photo-sphere-viewer/visible-range-plugin", + "version": "0.0.0", + "description": "Photo sphere Viewer plugin to lock the visible angles.", + "homepage": "https://photo-sphere-viewer.js.org/plugins/visible-range", + "license": "MIT", + "main": "./src/index.ts", + "types": "./src/index.ts", + "dependencies": { + "@photo-sphere-viewer/core": "0.0.0" + }, + "scripts": { + "build": "tsup", + "watch": "tsup --watch", + "lint": "eslint . --fix", + "publish-dist": "cd dist && npm publish --tag=$NPM_TAG --access=public" + }, + "psv": { + "globalName": "PhotoSphereViewer.VisibleRangePlugin" + }, + "typedoc": { + "displayName": "plugin: VisibleRange", + "readmeFile": "./.typedoc/README.md" + } +} diff --git a/packages/visible-range-plugin/src/VisibleRangePlugin.ts b/packages/visible-range-plugin/src/VisibleRangePlugin.ts new file mode 100644 index 000000000..1c39553bc --- /dev/null +++ b/packages/visible-range-plugin/src/VisibleRangePlugin.ts @@ -0,0 +1,314 @@ +import type { AutorotatePlugin } from '@photo-sphere-viewer/autorotate-plugin'; +import type { Position, Viewer } from '@photo-sphere-viewer/core'; +import { AbstractPlugin, events, utils } from '@photo-sphere-viewer/core'; +import { MathUtils } from 'three'; +import { Range, VisibleRangePluginConfig } from './model'; + +type RangeResult = { + rangedPosition: Position; + sidesReached: Record<'top' | 'left' | 'bottom' | 'right', boolean>; +}; + +const EPS = 0.000001; + +const getConfig = utils.getConfigParser( + { + longitudeRange: null, + latitudeRange: null, + verticalRange: null, + horizontalRange: null, + usePanoData: false, + }, + { + horizontalRange(horizontalRange, { rawConfig }) { + if (!utils.isNil(rawConfig.longitudeRange)) { + utils.logWarn(`longitudeRange is deprecated, use horizontalRange instead`); + return rawConfig.longitudeRange; + } else { + return horizontalRange; + } + }, + verticalRange(verticalRange, { rawConfig }) { + if (!utils.isNil(rawConfig.latitudeRange)) { + utils.logWarn(`latitudeRange is deprecated, use verticalRange instead`); + return rawConfig.latitudeRange; + } else { + return verticalRange; + } + }, + } +); + +/** + * Locks the visible angles + */ +export class VisibleRangePlugin extends AbstractPlugin { + static override readonly id = 'visible-range'; + + readonly config: VisibleRangePluginConfig; + + private autorotate?: AutorotatePlugin; + + constructor(viewer: Viewer, config: VisibleRangePluginConfig) { + super(viewer); + + this.config = getConfig(config); + } + + /** + * @internal + */ + override init() { + super.init(); + + this.autorotate = this.viewer.getPlugin('autorotate'); + + this.viewer.addEventListener(events.PanoramaLoadedEvent.type, this); + this.viewer.addEventListener(events.PositionUpdatedEvent.type, this); + this.viewer.addEventListener(events.ZoomUpdatedEvent.type, this); + this.viewer.addEventListener(events.BeforeAnimateEvent.type, this); + this.viewer.addEventListener(events.BeforeRotateEvent.type, this); + + this.setVerticalRange(this.config.verticalRange); + this.setHorizontalRange(this.config.horizontalRange); + } + + /** + * @internal + */ + override destroy() { + this.viewer.removeEventListener(events.PanoramaLoadedEvent.type, this); + this.viewer.removeEventListener(events.PositionUpdatedEvent.type, this); + this.viewer.removeEventListener(events.ZoomUpdatedEvent.type, this); + this.viewer.removeEventListener(events.BeforeAnimateEvent.type, this); + this.viewer.removeEventListener(events.BeforeRotateEvent.type, this); + + super.destroy(); + } + + /** + * @internal + */ + handleEvent(e: Event) { + switch (e.type) { + case events.PanoramaLoadedEvent.type: + if (this.config.usePanoData) { + this.setRangesFromPanoData(); + } + break; + + case events.BeforeRotateEvent.type: + case events.BeforeAnimateEvent.type: { + const e2 = e as events.BeforeAnimateEvent; + const { rangedPosition } = this.__applyRanges(e2.position, e2.zoomLevel); + e2.position = rangedPosition; + break; + } + + case events.PositionUpdatedEvent.type: { + const currentPosition = (e as events.PositionUpdatedEvent).position; + const { sidesReached, rangedPosition } = this.__applyRanges(currentPosition); + + if ((sidesReached.left || sidesReached.right) && this.autorotate?.isEnabled()) { + this.__reverseAutorotate(sidesReached.left, sidesReached.right); + } else if ( + Math.abs(currentPosition.yaw - rangedPosition.yaw) > EPS + || Math.abs(currentPosition.pitch - rangedPosition.pitch) > EPS + ) { + this.viewer.dynamics.position.setValue(rangedPosition); + } + break; + } + + case events.ZoomUpdatedEvent.type: { + const currentPosition = this.viewer.getPosition(); + const { rangedPosition } = this.__applyRanges(currentPosition); + + if ( + Math.abs(currentPosition.yaw - rangedPosition.yaw) > EPS + || Math.abs(currentPosition.pitch - rangedPosition.pitch) > EPS + ) { + this.viewer.dynamics.position.setValue(rangedPosition); + } + break; + } + } + } + + /** + * @deprecated Use {@link setHorizontalRange} + */ + setLongitudeRange(range: Range) { + utils.logWarn(`setLongitudeRange is deprecated, use setHorizontalRange instead`); + this.setHorizontalRange(range); + } + + /** + * @deprecated Use {@link setVerticalRange} + */ + setLatitudeRange(range: Range) { + utils.logWarn(`setLatitudeRange is deprecated, use setVerticalRange instead`); + this.setVerticalRange(range); + } + + /** + * Changes the vertical range + */ + setVerticalRange(range: Range) { + // range must have two values + if (range && range.length !== 2) { + utils.logWarn('vertical range must have exactly two elements'); + range = null; + } + + // vertical range is between -PI/2 and PI/2 + if (range) { + this.config.verticalRange = range.map((angle) => utils.parseAngle(angle, true)) as any; + + if (this.config.verticalRange[0] > this.config.verticalRange[1]) { + utils.logWarn('vertical range values must be ordered'); + this.config.verticalRange = [this.config.verticalRange[1], this.config.verticalRange[0]] as any; + } + + if (this.viewer.state.ready) { + this.viewer.rotate(this.viewer.getPosition()); + } + } else { + this.config.verticalRange = null; + } + } + + /** + * Changes the horizontal range + */ + setHorizontalRange(range: Range) { + // horizontal range must have two values + if (range && range.length !== 2) { + utils.logWarn('horizontal range must have exactly two elements'); + range = null; + } + + // horizontal range is between 0 and 2*PI + if (range) { + this.config.horizontalRange = range.map((angle) => utils.parseAngle(angle)) as any; + + if (this.viewer.state.ready) { + this.viewer.rotate(this.viewer.getPosition()); + } + } else { + this.config.horizontalRange = null; + } + } + + /** + * Changes the ranges according the current panorama cropping data + */ + setRangesFromPanoData() { + this.setVerticalRange(this.__getPanoVerticalRange()); + this.setHorizontalRange(this.__getPanoHorizontalRange()); + } + + /** + * Gets the vertical range defined by the viewer's panoData + */ + private __getPanoVerticalRange(): Range { + const p = this.viewer.state.panoData; + if (p.croppedHeight === p.fullHeight) { + return null; + } else { + const getAngle = (y: number) => Math.PI * (1 - y / p.fullHeight) - Math.PI / 2; + return [getAngle(p.croppedY + p.croppedHeight), getAngle(p.croppedY)]; + } + } + + /** + * Gets the horizontal range defined by the viewer's panoData + */ + private __getPanoHorizontalRange(): Range { + const p = this.viewer.state.panoData; + if (p.croppedWidth === p.fullWidth) { + return null; + } else { + const getAngle = (x: number) => 2 * Math.PI * (x / p.fullWidth) - Math.PI; + return [getAngle(p.croppedX), getAngle(p.croppedX + p.croppedWidth)]; + } + } + + /** + * Apply "horizontalRange" and "verticalRange" + */ + private __applyRanges( + position: Position = this.viewer.getPosition(), + zoomLevel: number = this.viewer.getZoomLevel() + ): RangeResult { + const rangedPosition: Position = { yaw: position.yaw, pitch: position.pitch }; + const sidesReached: Record = {}; + + const vFov = this.viewer.dataHelper.zoomLevelToFov(zoomLevel); + const hFov = this.viewer.dataHelper.vFovToHFov(vFov); + + if (this.config.horizontalRange) { + const range = utils.clone(this.config.horizontalRange) as any; + const offset = MathUtils.degToRad(hFov) / 2; + + range[0] = utils.parseAngle(range[0] + offset); + range[1] = utils.parseAngle(range[1] - offset); + + if (range[0] > range[1]) { + // when the range cross horizontal origin + if (position.yaw > range[1] && position.yaw < range[0]) { + if (position.yaw > range[0] / 2 + range[1] / 2) { + // detect which side we are closer too + rangedPosition.yaw = range[0]; + sidesReached.left = true; + } else { + rangedPosition.yaw = range[1]; + sidesReached.right = true; + } + } + } else if (position.yaw < range[0]) { + rangedPosition.yaw = range[0]; + sidesReached.left = true; + } else if (position.yaw > range[1]) { + rangedPosition.yaw = range[1]; + sidesReached.right = true; + } + } + + if (this.config.verticalRange) { + const range = utils.clone(this.config.verticalRange) as any; + const offset = MathUtils.degToRad(vFov) / 2; + + range[0] = utils.parseAngle(range[0] + offset, true); + range[1] = utils.parseAngle(range[1] - offset, true); + + // for very a narrow images, lock the horizontal angle to the center + if (range[0] > range[1]) { + range[0] = (range[0] + range[1]) / 2; + range[1] = range[0]; + } + + if (position.pitch < range[0]) { + rangedPosition.pitch = range[0]; + sidesReached.bottom = true; + } else if (position.pitch > range[1]) { + rangedPosition.pitch = range[1]; + sidesReached.top = true; + } + } + + return { rangedPosition, sidesReached }; + } + + /** + * Reverses autorotate direction with smooth transition + */ + private __reverseAutorotate(left: boolean, right: boolean) { + // reverse already ongoing + if ((left && this.autorotate.config.autorotateSpeed > 0) || (right && this.autorotate.config.autorotateSpeed < 0)) { + return; + } + + this.autorotate.reverse(); + } +} diff --git a/packages/visible-range-plugin/src/index.ts b/packages/visible-range-plugin/src/index.ts new file mode 100644 index 000000000..ab2c30b9c --- /dev/null +++ b/packages/visible-range-plugin/src/index.ts @@ -0,0 +1,2 @@ +export * from './model'; +export { VisibleRangePlugin } from './VisibleRangePlugin'; diff --git a/packages/visible-range-plugin/src/model.ts b/packages/visible-range-plugin/src/model.ts new file mode 100644 index 000000000..041acb736 --- /dev/null +++ b/packages/visible-range-plugin/src/model.ts @@ -0,0 +1,25 @@ +export type Range = [number, number] | [string, string]; + +export type VisibleRangePluginConfig = { + /** + * @deprecated use `horizontalRange` instead + */ + longitudeRange?: Range; + /** + * @deprecated use `verticalRange` instead + */ + latitudeRange?: Range; + /** + * horizontal range as two angles + */ + horizontalRange?: Range; + /** + * vertical range as two angles + */ + verticalRange?: Range; + /** + * use {@link ViewerConfig.panoData} as visible range, you can also manually call {@link VisibleRangePlugin.setRangesFromPanoData} + * @default false + */ + usePanoData?: boolean; +}; diff --git a/packages/visible-range-plugin/tsconfig.json b/packages/visible-range-plugin/tsconfig.json new file mode 100644 index 000000000..ebd748fe4 --- /dev/null +++ b/packages/visible-range-plugin/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*.ts", "../shared/typings.d.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/visible-range-plugin/tsup.config.js b/packages/visible-range-plugin/tsup.config.js new file mode 100644 index 000000000..a1aaf53f1 --- /dev/null +++ b/packages/visible-range-plugin/tsup.config.js @@ -0,0 +1,4 @@ +import createConfig from '../../build/tsup.config'; +import pkg from './package.json' assert { type: 'json' }; + +export default createConfig(pkg); diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 49ca49604..000000000 --- a/rollup.config.js +++ /dev/null @@ -1,192 +0,0 @@ -import babel from '@rollup/plugin-babel'; -import json from '@rollup/plugin-json'; -import replace from '@rollup/plugin-replace'; -import fs from 'fs'; -import path from 'path'; -import localResolve from 'rollup-plugin-local-resolve'; -import postcss from 'rollup-plugin-postcss'; -import dts from 'rollup-plugin-dts'; -import { string } from 'rollup-plugin-string'; - -import pkg from './package.json'; - -const plugins = fs.readdirSync(path.join(__dirname, 'src/plugins')) - .filter(p => fs.lstatSync(`src/plugins/${p}`).isDirectory()) - .filter(p => p !== 'shared'); - -const adapters = fs.readdirSync(path.join(__dirname, 'src/adapters')) - .filter(p => fs.lstatSync(`src/adapters/${p}`).isDirectory()) - .filter(p => p !== 'equirectangular' && p !== 'shared'); - -const banner = `/*! -* Photo Sphere Viewer ${pkg.version} -* @copyright 2014-2015 Jérémy Heleine -* @copyright 2015-${new Date().getFullYear()} Damien "Mistic" Sorel -* @licence MIT (https://opensource.org/licenses/MIT) -*/`; - -const cssBanner = `Photo Sphere Viewer ${pkg.version} -@copyright 2014-2015 Jérémy Heleine -@copyright 2015-${new Date().getFullYear()} Damien "Mistic" Sorel -@licence MIT (https://opensource.org/licenses/MIT)`; - -function camelize(str) { - return str - .replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, m => m.toUpperCase()) - .replace(/(?:\W|_)/g, ''); -} - -const baseConfig = { - output : { - format : 'umd', - sourcemap: true, - interop : false, - banner : banner, - globals : { - 'three' : 'THREE', - 'uevent': 'uEvent', - }, - }, - external: [ - 'three', - 'uevent', - ], - plugins : () => [ - localResolve(), - babel({ - exclude : 'node_modules/**', - babelHelpers: 'bundled', - }), - postcss({ - extract : true, - sourceMap: true, - use : ['sass'], - plugins : [ - require('autoprefixer')({}), - require('postcss-banner')({ - banner : cssBanner, - important: true, - }), - ], - }), - string({ - include: [ - 'src/**/*.svg', - ], - }), - json({ - compact: true, - }), - ], -}; - -const secondaryConfig = { - ...baseConfig, - output : { - ...baseConfig.output, - globals: { - ...baseConfig.output.globals, - 'photo-sphere-viewer' : 'PhotoSphereViewer', - 'photo-sphere-viewer/dist/adapters/cubemap': 'PhotoSphereViewer.CubemapAdapter', - }, - }, - external: [ - ...baseConfig.external, - 'photo-sphere-viewer', - ], - plugins : () => [ - replace({ - delimiters : ['', ''], - preventAssignment : true, - [`from '../..'`] : `from 'photo-sphere-viewer'`, - [`from '../cubemap'`]: `from 'photo-sphere-viewer/dist/adapters/cubemap'`, - }), - ...baseConfig.plugins(), - ], -}; - -const baseConfigDTS = { - output : { - format: 'es', - }, - plugins: () => [ - dts(), - ], -}; - -const secondaryConfigDTS = { - ...baseConfigDTS, - external: [ - ...secondaryConfig.external, - ], - plugins : () => [ - replace({ - delimiters : ['', ''], - preventAssignment : true, - [`from '../..'`] : `from 'photo-sphere-viewer'`, - [`from '../markers'`]: `from 'photo-sphere-viewer/dist/plugins/markers'`, - [`from '../cubemap'`]: `from 'photo-sphere-viewer/dist/adapters/cubemap'`, - }), - ...baseConfigDTS.plugins(), - ], -}; - -export default [ - { - ...baseConfig, - input : 'src/index.js', - output : { - ...baseConfig.output, - file: 'dist/photo-sphere-viewer.js', - name: 'PhotoSphereViewer', - }, - plugins: baseConfig.plugins(), - }, - { - ...baseConfigDTS, - input : 'types/index.d.ts', - output : { - ...baseConfigDTS.output, - file: 'dist/photo-sphere-viewer.d.ts', - }, - plugins: baseConfigDTS.plugins(), - }, - ...plugins.map(p => ({ - ...secondaryConfig, - input : `src/plugins/${p}/index.js`, - output : { - ...secondaryConfig.output, - file: `dist/plugins/${p}.js`, - name: `PhotoSphereViewer.${camelize(p)}Plugin`, - }, - plugins: secondaryConfig.plugins(), - })), - ...adapters.map(p => ({ - ...secondaryConfig, - input : `src/adapters/${p}/index.js`, - output : { - ...secondaryConfig.output, - file: `dist/adapters/${p}.js`, - name: `PhotoSphereViewer.${camelize(p)}Adapter`, - }, - plugins: secondaryConfig.plugins(), - })), - ...plugins.map(p => ({ - ...secondaryConfigDTS, - input : `types/plugins/${p}/index.d.ts`, - output : { - ...secondaryConfigDTS.output, - file: `dist/plugins/${p}.d.ts`, - }, - plugins: secondaryConfigDTS.plugins(), - })), - ...adapters.map(p => ({ - ...secondaryConfigDTS, - input : `types/adapters/${p}/index.d.ts`, - output : { - ...secondaryConfigDTS.output, - file: `dist/adapters/${p}.d.ts`, - }, - plugins: secondaryConfigDTS.plugins(), - })) -]; diff --git a/src/PSVError.js b/src/PSVError.js deleted file mode 100644 index ada8ae004..000000000 --- a/src/PSVError.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @summary Custom error used in the lib - * @param {string} message - * @constructor - * @memberOf PSV - */ -function PSVError(message) { - this.message = message; - - // Use V8's native method if available, otherwise fallback - if ('captureStackTrace' in Error) { - Error.captureStackTrace(this, PSVError); - } - else { - this.stack = (new Error()).stack; - } -} - -PSVError.prototype = Object.create(Error.prototype); -PSVError.prototype.name = 'PSVError'; -PSVError.prototype.constructor = PSVError; - -export { PSVError }; diff --git a/src/Viewer.js b/src/Viewer.js deleted file mode 100644 index f8a14c397..000000000 --- a/src/Viewer.js +++ /dev/null @@ -1,1061 +0,0 @@ -import { Cache, MathUtils, Vector3 } from 'three'; -import { EventEmitter } from 'uevent'; -import { Loader } from './components/Loader'; -import { Navbar } from './components/Navbar'; -import { Notification } from './components/Notification'; -import { Overlay } from './components/Overlay'; -import { Panel } from './components/Panel'; -import { CONFIG_PARSERS, DEFAULTS, DEPRECATED_OPTIONS, getConfig, READONLY_OPTIONS } from './data/config'; -import { - ANIMATION_MIN_DURATION, - CHANGE_EVENTS, - DEFAULT_TRANSITION, - EVENTS, - IDS, - SPHERE_RADIUS, - VIEWER_DATA -} from './data/constants'; -import { SYSTEM } from './data/system'; -import errorIcon from './icons/error.svg'; -import { AbstractPlugin } from './plugins/AbstractPlugin'; -import { PSVError } from './PSVError'; -import { DataHelper } from './services/DataHelper'; -import { EventsHandler } from './services/EventsHandler'; -import { Renderer } from './services/Renderer'; -import { TextureLoader } from './services/TextureLoader'; -import { TooltipRenderer } from './services/TooltipRenderer'; -import { - Animation, - Dynamic, - each, - exitFullscreen, - getAbortError, - getAngle, - getShortestArc, - isAbortError, - isExtendedPosition, - isFullscreenEnabled, - isNil, - logWarn, - MultiDynamic, - pluginInterop, - requestFullscreen, - throttle, - toggleClass -} from './utils'; - -Cache.enabled = true; - -/** - * @summary Main class - * @memberOf PSV - * @extends {external:uEvent.EventEmitter} - */ -export class Viewer extends EventEmitter { - - /** - * @param {PSV.Options} options - * @fires PSV.ready - * @throws {PSV.PSVError} when the configuration is incorrect - */ - constructor(options) { - super(); - - SYSTEM.load(); - - // must support WebGL - if (!SYSTEM.isWebGLSupported) { - throw new PSVError('WebGL is not supported.'); - } - - if (SYSTEM.maxTextureWidth === 0) { - throw new PSVError('Unable to detect system capabilities'); - } - - /** - * @summary Internal properties - * @member {Object} - * @protected - * @property {boolean} ready - when all components are loaded - * @property {boolean} uiRefresh - if the UI needs to be renderer - * @property {boolean} needsUpdate - if the view needs to be renderer - * @property {boolean} fullscreen - if the viewer is currently fullscreen - * @property {external:THREE.Vector3} direction - direction of the camera - * @property {number} vFov - vertical FOV - * @property {number} hFov - horizontal FOV - * @property {number} aspect - viewer aspect ratio - * @property {boolean} autorotateEnabled - automatic rotation is enabled - * @property {PSV.utils.Animation} animationPromise - promise of the current animation - * @property {Promise} loadingPromise - promise of the setPanorama method - * @property {boolean} littlePlanet - special tweaks for LittlePlanetAdapter - * @property {number} idleTime - time of the last user action - * @property {object} objectsObservers - * @property {PSV.Size} size - size of the container - * @property {PSV.PanoData} panoData - panorama metadata, if supported - */ - this.prop = { - ready : false, - uiRefresh : false, - needsUpdate : false, - fullscreen : false, - direction : new Vector3(0, 0, SPHERE_RADIUS), - vFov : null, - hFov : null, - aspect : null, - autorotateEnabled: false, - animationPromise : null, - loadingPromise : null, - littlePlanet : false, - idleTime : -1, - objectsObservers : {}, - size : { - width : 0, - height: 0, - }, - panoData : { - fullWidth : 0, - fullHeight : 0, - croppedWidth : 0, - croppedHeight: 0, - croppedX : 0, - croppedY : 0, - poseHeading : 0, - posePitch : 0, - poseRoll : 0, - }, - }; - - /** - * @summary Configuration holder - * @type {PSV.Options} - * @readonly - */ - this.config = getConfig(options); - - /** - * @summary Top most parent - * @member {HTMLElement} - * @readonly - */ - this.parent = (typeof options.container === 'string') ? document.getElementById(options.container) : options.container; - this.parent[VIEWER_DATA] = this; - - /** - * @summary Main container - * @member {HTMLElement} - * @readonly - */ - this.container = document.createElement('div'); - this.container.classList.add('psv-container'); - this.parent.appendChild(this.container); - - /** - * @summary Render adapter - * @type {PSV.adapters.AbstractAdapter} - * @readonly - * @package - */ - this.adapter = new this.config.adapter[0](this, this.config.adapter[1]); // eslint-disable-line new-cap - - /** - * @summary All child components - * @type {PSV.components.AbstractComponent[]} - * @readonly - * @package - */ - this.children = []; - - /** - * @summary All plugins - * @type {Object} - * @readonly - * @package - */ - this.plugins = {}; - - /** - * @type {PSV.services.Renderer} - * @readonly - */ - this.renderer = new Renderer(this); - - /** - * @type {PSV.services.TextureLoader} - * @readonly - */ - this.textureLoader = new TextureLoader(this); - - /** - * @type {PSV.services.EventsHandler} - * @readonly - */ - this.eventsHandler = new EventsHandler(this); - - /** - * @type {PSV.services.DataHelper} - * @readonly - */ - this.dataHelper = new DataHelper(this); - - /** - * @member {PSV.components.Loader} - * @readonly - */ - this.loader = new Loader(this); - - /** - * @member {PSV.components.Navbar} - * @readonly - */ - this.navbar = new Navbar(this); - - /** - * @member {PSV.components.Panel} - * @readonly - */ - this.panel = new Panel(this); - - /** - * @member {PSV.services.TooltipRenderer} - * @readonly - */ - this.tooltip = new TooltipRenderer(this); - - /** - * @member {PSV.components.Notification} - * @readonly - */ - this.notification = new Notification(this); - - /** - * @member {PSV.components.Overlay} - * @readonly - */ - this.overlay = new Overlay(this); - - /** - * @member {Record} - * @package - */ - this.dynamics = { - zoom: new Dynamic((value) => { - this.prop.vFov = this.dataHelper.zoomLevelToFov(value); - this.prop.hFov = this.dataHelper.vFovToHFov(this.prop.vFov); - this.trigger(EVENTS.ZOOM_UPDATED, value); - }, this.config.defaultZoomLvl, 0, 100), - - position: new MultiDynamic({ - longitude: new Dynamic(null, this.config.defaultLong, 0, 2 * Math.PI, true), - latitude : this.prop.littlePlanet - ? new Dynamic(null, this.config.defaultLat, 0, Math.PI * 2, true) - : new Dynamic(null, this.config.defaultLat, -Math.PI / 2, Math.PI / 2), - }, (position) => { - this.dataHelper.sphericalCoordsToVector3(position, this.prop.direction); - this.trigger(EVENTS.POSITION_UPDATED, position); - }), - }; - - this.__updateSpeeds(); - - this.eventsHandler.init(); - - this.__resizeRefresh = throttle(() => this.refreshUi('resize'), 500); - - // apply container size - this.resize(this.config.size); - - // init plugins - this.config.plugins.forEach(([plugin, opts]) => { - this.plugins[plugin.id] = new plugin(this, opts); // eslint-disable-line new-cap - }); - each(this.plugins, plugin => plugin.init?.()); - - // init buttons - this.navbar.setButtons(this.config.navbar); - - // load panorama - if (this.config.panorama) { - this.setPanorama(this.config.panorama); - } - - toggleClass(this.container, 'psv--is-touch', SYSTEM.isTouchEnabled.initial); - SYSTEM.isTouchEnabled.promise.then(enabled => toggleClass(this.container, 'psv--is-touch', enabled)); - - // enable GUI after first render - this.once(EVENTS.RENDER, () => { - if (this.config.navbar) { - this.container.classList.add('psv--has-navbar'); - this.navbar.show(); - } - - // Queue autorotate - if (!isNil(this.config.autorotateDelay)) { - this.prop.idleTime = performance.now(); - } - - this.prop.ready = true; - - setTimeout(() => { - this.refreshUi('init'); - - this.trigger(EVENTS.READY); - }, 0); - }); - } - - /** - * @summary Destroys the viewer - * @description The memory used by the ThreeJS context is not totally cleared. This will be fixed as soon as possible. - */ - destroy() { - this.__stopAll(); - this.stopKeyboardControl(); - this.exitFullscreen(); - - each(this.plugins, plugin => plugin.destroy()); - delete this.plugins; - - this.children.slice().forEach(child => child.destroy()); - this.children.length = 0; - - this.eventsHandler.destroy(); - this.renderer.destroy(); - this.textureLoader.destroy(); - this.dataHelper.destroy(); - this.adapter.destroy(); - - this.parent.removeChild(this.container); - delete this.parent[VIEWER_DATA]; - - delete this.parent; - delete this.container; - - delete this.loader; - delete this.navbar; - delete this.panel; - delete this.tooltip; - delete this.notification; - delete this.overlay; - delete this.dynamics; - - delete this.config; - } - - /** - * @summary Refresh UI - * @package - */ - refreshUi(reason) { - if (!this.prop.ready) { - return; - } - - if (!this.prop.uiRefresh) { - // console.log(`PhotoSphereViewer: UI Refresh, ${reason}`); - - this.prop.uiRefresh = true; - - this.children.every((child) => { - child.refreshUi(); - return this.prop.uiRefresh === true; - }); - - this.prop.uiRefresh = false; - } - else if (this.prop.uiRefresh !== 'new') { - this.prop.uiRefresh = 'new'; - - // wait for current refresh to cancel - setTimeout(() => { - this.prop.uiRefresh = false; - this.refreshUi(reason); - }); - } - } - - /** - * @summary Returns the instance of a plugin if it exists - * @param {Class|string} pluginId - * @returns {PSV.plugins.AbstractPlugin} - */ - getPlugin(pluginId) { - if (typeof pluginId === 'string') { - return this.plugins[pluginId]; - } - else { - const pluginCtor = pluginInterop(pluginId, AbstractPlugin); - return pluginCtor ? this.plugins[pluginCtor.id] : undefined; - } - } - - /** - * @summary Returns the current position of the camera - * @returns {PSV.Position} - */ - getPosition() { - return this.dataHelper.cleanPosition(this.dynamics.position.current); - } - - /** - * @summary Returns the current zoom level - * @returns {number} - */ - getZoomLevel() { - return this.dynamics.zoom.current; - } - - /** - * @summary Returns the current viewer size - * @returns {PSV.Size} - */ - getSize() { - return { ...this.prop.size }; - } - - /** - * @summary Checks if the automatic rotation is enabled - * @returns {boolean} - */ - isAutorotateEnabled() { - return this.prop.autorotateEnabled; - } - - /** - * @summary Checks if the viewer is in fullscreen - * @returns {boolean} - */ - isFullscreenEnabled() { - if (SYSTEM.fullscreenEvent) { - return isFullscreenEnabled(this.container); - } - else { - return this.prop.fullscreen; - } - } - - /** - * @summary Flags the view has changed for the next render - */ - needsUpdate() { - this.prop.needsUpdate = true; - } - - /** - * @summary Resizes the canvas when the window is resized - * @fires PSV.size-updated - */ - autoSize() { - if (this.container.clientWidth !== this.prop.size.width || this.container.clientHeight !== this.prop.size.height) { - this.prop.size.width = Math.round(this.container.clientWidth); - this.prop.size.height = Math.round(this.container.clientHeight); - this.prop.aspect = this.prop.size.width / this.prop.size.height; - this.prop.hFov = this.dataHelper.vFovToHFov(this.prop.vFov); - - this.trigger(EVENTS.SIZE_UPDATED, this.getSize()); - this.__resizeRefresh(); - } - } - - /** - * @summary Loads a new panorama file - * @description Loads a new panorama file, optionally changing the camera position/zoom and activating the transition animation.
- * If the "options" parameter is not defined, the camera will not move and the ongoing animation will continue.
- * If another loading is already in progress it will be aborted. - * @param {*} path - URL of the new panorama file - * @param {PSV.PanoramaOptions} [options] - * @returns {Promise} resolves false if the loading was aborted by another call - */ - setPanorama(path, options = {}) { - this.textureLoader.abortLoading(); - this.prop.transitionAnimation?.cancel(); - - // apply default parameters on first load - if (!this.prop.ready) { - ['sphereCorrection', 'panoData', 'overlay', 'overlayOpacity'].forEach((opt) => { - if (!(opt in options)) { - options[opt] = this.config[opt]; - } - }); - } - - if (options.transition === undefined || options.transition === true) { - options.transition = DEFAULT_TRANSITION; - } - if (options.showLoader === undefined) { - options.showLoader = true; - } - if (options.caption === undefined) { - options.caption = this.config.caption; - } - if (options.description === undefined) { - options.description = this.config.description; - } - if (!options.panoData && typeof this.config.panoData === 'function') { - options.panoData = this.config.panoData; - } - - const positionProvided = isExtendedPosition(options); - const zoomProvided = 'zoom' in options; - - if (positionProvided || zoomProvided) { - this.__stopAll(); - } - - this.hideError(); - - this.config.panorama = path; - this.config.caption = options.caption; - this.config.description = options.description; - - const done = (err) => { - this.loader.hide(); - - this.prop.loadingPromise = null; - - if (isAbortError(err)) { - return false; - } - else if (err) { - this.navbar.setCaption(''); - this.showError(this.config.lang.loadError); - console.error(err); - throw err; - } - else { - this.resetIdleTimer(); - this.setOverlay(options.overlay, options.overlayOpacity); - this.navbar.setCaption(this.config.caption); - return true; - } - }; - - this.navbar.setCaption(`${this.config.loadingTxt || ''}`); - if (options.showLoader || !this.prop.ready) { - this.loader.show(); - } - - const loadingPromise = this.adapter.loadTexture(this.config.panorama, options.panoData) - .then((textureData) => { - // check if another panorama was requested - if (textureData.panorama !== this.config.panorama) { - this.adapter.disposeTexture(textureData); - throw getAbortError(); - } - return textureData; - }); - - if (!options.transition || !this.prop.ready || !this.adapter.supportsTransition(this.config.panorama)) { - this.prop.loadingPromise = loadingPromise - .then((textureData) => { - this.renderer.show(); - this.renderer.setTexture(textureData); - this.renderer.setPanoramaPose(textureData.panoData); - this.renderer.setSphereCorrection(options.sphereCorrection); - - if (zoomProvided) { - this.zoom(options.zoom); - } - if (positionProvided) { - this.rotate(options); - } - }) - .then(done, done); - } - else { - this.prop.loadingPromise = loadingPromise - .then((textureData) => { - this.loader.hide(); - - this.prop.transitionAnimation = this.renderer.transition(textureData, options); - return this.prop.transitionAnimation; - }) - .then((completed) => { - this.prop.transitionAnimation = null; - if (!completed) { - throw getAbortError(); - } - }) - .then(done, done); - } - - return this.prop.loadingPromise; - } - - /** - * @summary Loads a new overlay - * @param {*} path - URL of the new overlay file - * @param {number} [opacity=1] - * @returns {Promise} - */ - setOverlay(path, opacity = 1) { - if (!path) { - if (this.adapter.constructor.supportsOverlay) { - this.renderer.setOverlay(null, 0); - } - - return Promise.resolve(); - } - else { - if (!this.adapter.constructor.supportsOverlay) { - return Promise.reject(new PSVError(`${this.adapter.constructor.id} adapter does not supports overlay`)); - } - - return this.adapter.loadTexture(path, (image) => { - const p = this.prop.panoData; - const r = image.width / p.croppedWidth; - return { - fullWidth : r * p.fullWidth, - fullHeight : r * p.fullHeight, - croppedWidth : r * p.croppedWidth, - croppedHeight: r * p.croppedHeight, - croppedX : r * p.croppedX, - croppedY : r * p.croppedY, - }; - }, false) - .then((textureData) => { - this.renderer.setOverlay(textureData, opacity); - }); - } - } - - /** - * @summary Update options - * @param {PSV.Options} options - * @fires PSV.config-changed - * @throws {PSV.PSVError} when the configuration is incorrect - */ - setOptions(options) { - const rawConfig = { - ...this.config, - ...options, - }; - - each(options, (value, key) => { - if (DEPRECATED_OPTIONS[key]) { - logWarn(DEPRECATED_OPTIONS[key]); - return; - } - - if (!Object.prototype.hasOwnProperty.call(DEFAULTS, key)) { - throw new PSVError(`Unknown option ${key}`); - } - - if (READONLY_OPTIONS[key]) { - throw new PSVError(READONLY_OPTIONS[key]); - } - - if (CONFIG_PARSERS[key]) { - this.config[key] = CONFIG_PARSERS[key](value, rawConfig); - } - else { - this.config[key] = value; - } - - switch (key) { - case 'overlay': - case 'overlayOpacity': - this.setOverlay(this.config.overlay, this.config.overlayOpacity); - break; - - case 'caption': - case 'description': - this.navbar.setCaption(this.config.caption); - break; - - case 'size': - this.resize(value); - break; - - case 'sphereCorrection': - this.renderer.setSphereCorrection(value); - break; - - case 'navbar': - case 'lang': - this.navbar.setButtons(this.config.navbar); - break; - - case 'moveSpeed': - case 'zoomSpeed': - this.__updateSpeeds(); - break; - - case 'minFov': - case 'maxFov': - this.dynamics.zoom.setValue(this.dataHelper.fovToZoomLevel(this.prop.vFov)); - this.trigger(EVENTS.ZOOM_UPDATED, this.getZoomLevel()); - break; - - case 'canvasBackground': - this.renderer.canvasContainer.style.background = this.config.canvasBackground; - break; - - case 'autorotateIdle': - this.resetIdleTimer(); - break; - - default: - break; - } - }); - - this.needsUpdate(); - this.refreshUi('set options'); - - this.trigger(EVENTS.CONFIG_CHANGED, Object.keys(options)); - } - - /** - * @summary Update options - * @param {string} option - * @param {any} value - * @fires PSV.config-changed - * @throws {PSV.PSVError} when the configuration is incorrect - */ - setOption(option, value) { - this.setOptions({ [option]: value }); - } - - /** - * @summary Restarts the idle timer (if `autorotateIdle=true`) - * @package - */ - resetIdleTimer() { - this.prop.idleTime = this.config.autorotateIdle ? performance.now() : -1; - } - - /** - * @summary Stops the idle timer - * @package - */ - disableIdleTimer() { - this.prop.idleTime = -1; - } - - /** - * @summary Starts the automatic rotation - * @fires PSV.autorotate - */ - startAutorotate(refresh = false) { - if (refresh && !this.isAutorotateEnabled()) { - return; - } - if (!refresh && this.isAutorotateEnabled()) { - return; - } - - if (!refresh) { - this.__stopAll(); - } - - this.dynamics.position.roll({ - longitude: this.config.autorotateSpeed < 0, - }, Math.abs(this.config.autorotateSpeed / this.config.moveSpeed)); - - this.dynamics.position.goto({ - latitude: this.config.autorotateLat, - }, Math.abs(this.config.autorotateSpeed / this.config.moveSpeed)); - - if (this.config.autorotateZoomLvl !== null) { - this.dynamics.zoom.goto(this.config.autorotateZoomLvl); - } - - this.prop.autorotateEnabled = true; - - if (!refresh) { - this.trigger(EVENTS.AUTOROTATE, true); - } - } - - /** - * @summary Stops the automatic rotation - * @fires PSV.autorotate - */ - stopAutorotate() { - if (this.isAutorotateEnabled()) { - this.dynamics.position.stop(); - this.dynamics.zoom.stop(); - - this.prop.autorotateEnabled = false; - - this.trigger(EVENTS.AUTOROTATE, false); - } - } - - /** - * @summary Starts or stops the automatic rotation - * @fires PSV.autorotate - */ - toggleAutorotate() { - if (this.isAutorotateEnabled()) { - this.stopAutorotate(); - } - else { - this.startAutorotate(); - } - } - - /** - * @summary Displays an error message over the viewer - * @param {string} message - */ - showError(message) { - this.overlay.show({ - id : IDS.ERROR, - image : errorIcon, - text : message, - dissmisable: false, - }); - } - - /** - * @summary Hides the error message - */ - hideError() { - this.overlay.hide(IDS.ERROR); - } - - /** - * @summary Rotates the view to specific longitude and latitude - * @param {PSV.ExtendedPosition} position - * @fires PSV.before-rotate - * @fires PSV.position-updated - */ - rotate(position) { - const e = this.trigger(EVENTS.BEFORE_ROTATE, position); - if (e.isDefaultPrevented()) { - return; - } - - const cleanPosition = this.change(CHANGE_EVENTS.GET_ROTATE_POSITION, this.dataHelper.cleanPosition(position)); - this.dynamics.position.setValue(cleanPosition); - } - - /** - * @summary Rotates and zooms the view with a smooth animation - * @param {PSV.AnimateOptions} options - position and/or zoom level - * @returns {PSV.utils.Animation} - */ - animate(options) { - this.__stopAll(); - - const positionProvided = isExtendedPosition(options); - const zoomProvided = options.zoom !== undefined; - - const animProperties = {}; - let duration; - - // clean/filter position and compute duration - if (positionProvided) { - const cleanPosition = this.change(CHANGE_EVENTS.GET_ANIMATE_POSITION, this.dataHelper.cleanPosition(options)); - const currentPosition = this.getPosition(); - - // longitude offset for shortest arc - const tOffset = getShortestArc(currentPosition.longitude, cleanPosition.longitude); - - animProperties.longitude = { start: currentPosition.longitude, end: currentPosition.longitude + tOffset }; - animProperties.latitude = { start: currentPosition.latitude, end: cleanPosition.latitude }; - - duration = this.dataHelper.speedToDuration(options.speed, getAngle(currentPosition, cleanPosition)); - } - - // clean/filter zoom and compute duration - if (zoomProvided) { - const dZoom = Math.abs(options.zoom - this.getZoomLevel()); - - animProperties.zoom = { start: this.getZoomLevel(), end: options.zoom }; - - if (!duration) { - // if animating zoom only and a speed is given, use an arbitrary PI/4 to compute the duration - duration = this.dataHelper.speedToDuration(options.speed, Math.PI / 4 * dZoom / 100); - } - } - - // if no animation needed - if (!duration) { - if (positionProvided) { - this.rotate(options); - } - if (zoomProvided) { - this.zoom(options.zoom); - } - - return new Animation(); - } - - this.prop.animationPromise = new Animation({ - properties: animProperties, - duration : Math.max(ANIMATION_MIN_DURATION, duration), - easing : 'inOutSine', - onTick : (properties) => { - if (positionProvided) { - this.rotate(properties); - } - if (zoomProvided) { - this.zoom(properties.zoom); - } - }, - }); - - this.prop.animationPromise.then(() => { - this.prop.animationPromise = null; - this.resetIdleTimer(); - }); - - return this.prop.animationPromise; - } - - /** - * @summary Stops the ongoing animation - * @description The return value is a Promise because the is no guaranty the animation can be stopped synchronously. - * @returns {Promise} Resolved when the animation has ben cancelled - */ - stopAnimation() { - if (this.prop.animationPromise) { - this.prop.animationPromise.cancel(); - return this.prop.animationPromise; - } - else { - return Promise.resolve(); - } - } - - /** - * @summary Zooms to a specific level between `max_fov` and `min_fov` - * @param {number} level - new zoom level from 0 to 100 - * @fires PSV.zoom-updated - */ - zoom(level) { - this.dynamics.zoom.setValue(level); - } - - /** - * @summary Increases the zoom level - * @param {number} [step=1] - */ - zoomIn(step = 1) { - this.dynamics.zoom.step(step); - } - - /** - * @summary Decreases the zoom level - * @param {number} [step=1] - */ - zoomOut(step = 1) { - this.dynamics.zoom.step(-step); - } - - /** - * @summary Resizes the viewer - * @param {PSV.CssSize} size - */ - resize(size) { - ['width', 'height'].forEach((dim) => { - if (size && size[dim]) { - if (/^[0-9.]+$/.test(size[dim])) { - size[dim] += 'px'; - } - this.parent.style[dim] = size[dim]; - } - }); - - this.autoSize(); - } - - /** - * @summary Enters the fullscreen mode - * @fires PSV.fullscreen-updated - */ - enterFullscreen() { - if (SYSTEM.fullscreenEvent) { - requestFullscreen(this.container); - } - else { - this.container.classList.add('psv-container--fullscreen'); - this.autoSize(); - this.eventsHandler.__fullscreenToggled(true); - } - } - - /** - * @summary Exits the fullscreen mode - * @fires PSV.fullscreen-updated - */ - exitFullscreen() { - if (this.isFullscreenEnabled()) { - if (SYSTEM.fullscreenEvent) { - exitFullscreen(); - } - else { - this.container.classList.remove('psv-container--fullscreen'); - this.autoSize(); - this.eventsHandler.__fullscreenToggled(false); - } - } - } - - /** - * @summary Enters or exits the fullscreen mode - * @fires PSV.fullscreen-updated - */ - toggleFullscreen() { - if (!this.isFullscreenEnabled()) { - this.enterFullscreen(); - } - else { - this.exitFullscreen(); - } - } - - /** - * @summary Enables the keyboard controls (done automatically when entering fullscreen) - */ - startKeyboardControl() { - this.eventsHandler.enableKeyboard(); - } - - /** - * @summary Disables the keyboard controls (done automatically when exiting fullscreen) - */ - stopKeyboardControl() { - this.eventsHandler.disableKeyboard(); - } - - /** - * @summary Subscribes to events on objects in the scene - * @param {string} userDataKey - only objects with the following `userData` will be emitted - * @param {EventListener} listener - must implement `handleEvent` - * @return {function} call to stop the subscription - * @package - */ - observeObjects(userDataKey, listener) { - this.prop.objectsObservers[userDataKey] = { listener }; - - return () => { - delete this.prop.objectsObservers[userDataKey]; - }; - } - - /** - * @summary Stops all current animations - * @returns {Promise} - * @package - */ - __stopAll() { - this.trigger(EVENTS.STOP_ALL); - - this.disableIdleTimer(); - this.stopAutorotate(); - return this.stopAnimation(); - } - - /** - * @summary Recomputes dynamics speeds - * @private - */ - __updateSpeeds() { - this.dynamics.zoom.setSpeed(this.config.zoomSpeed * 50); - this.dynamics.position.setSpeed(MathUtils.degToRad(this.config.moveSpeed * 50)); - } - -} diff --git a/src/adapters/AbstractAdapter.js b/src/adapters/AbstractAdapter.js deleted file mode 100644 index 8f2807fbe..000000000 --- a/src/adapters/AbstractAdapter.js +++ /dev/null @@ -1,188 +0,0 @@ -import { ShaderMaterial, Texture } from 'three'; -import { PSVError } from '../PSVError'; - -/** - * @namespace PSV.adapters - */ - - -/** - * @summary Base adapters class - * @memberof PSV.adapters - * @abstract - */ -export class AbstractAdapter { - - /** - * @summary Unique identifier of the adapter - * @member {string} - * @readonly - * @static - */ - static id = null; - - /** - * @summary Indicates if the adapter supports panorama download natively - * @type {boolean} - * @readonly - * @static - */ - static supportsDownload = false; - - /** - * @summary Indicated if the adapter can display an additional transparent image above the panorama - * @type {boolean} - */ - static supportsOverlay = false; - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - /** - * @summary Reference to main controller - * @type {PSV.Viewer} - * @readonly - */ - this.psv = psv; - } - - /** - * @summary Destroys the adapter - */ - destroy() { - delete this.psv; - } - - /** - * @summary Indicates if the adapter supports transitions between panoramas - * @param {*} panorama - * @return {boolean} - */ - supportsTransition(panorama) { // eslint-disable-line no-unused-vars - return false; - } - - /** - * @summary Indicates if the adapter supports preload of a panorama - * @param {*} panorama - * @return {boolean} - */ - supportsPreload(panorama) { // eslint-disable-line no-unused-vars - return false; - } - - /** - * @abstract - * @summary Loads the panorama texture(s) - * @param {*} panorama - * @param {PSV.PanoData | PSV.PanoDataProvider} [newPanoData] - * @param {boolean} [useXmpPanoData] - * @returns {Promise.} - */ - loadTexture(panorama, newPanoData, useXmpPanoData) { // eslint-disable-line no-unused-vars - throw new PSVError('loadTexture not implemented'); - } - - /** - * @abstract - * @summary Creates the cube mesh - * @param {number} [scale=1] - * @returns {external:THREE.Mesh} - */ - createMesh(scale = 1) { // eslint-disable-line no-unused-vars - throw new PSVError('createMesh not implemented'); - } - - /** - * @abstract - * @summary Applies the texture to the mesh - * @param {external:THREE.Mesh} mesh - * @param {PSV.TextureData} textureData - * @param {boolean} [transition=false] - */ - setTexture(mesh, textureData, transition = false) { // eslint-disable-line no-unused-vars - throw new PSVError('setTexture not implemented'); - } - - /** - * @abstract - * @summary Changes the opacity of the mesh - * @param {external:THREE.Mesh} mesh - * @param {number} opacity - */ - setTextureOpacity(mesh, opacity) { // eslint-disable-line no-unused-vars - throw new PSVError('setTextureOpacity not implemented'); - } - - /** - * @abstract - * @summary Clear a loaded texture from memory - * @param {PSV.TextureData} textureData - */ - disposeTexture(textureData) { // eslint-disable-line no-unused-vars - throw new PSVError('disposeTexture not implemented'); - } - - /** - * @abstract - * @summary Applies the overlay to the mesh - * @param {external:THREE.Mesh} mesh - * @param {PSV.TextureData} textureData - * @param {number} opacity - */ - setOverlay(mesh, textureData, opacity) { // eslint-disable-line no-unused-vars - throw new PSVError('setOverlay not implemented'); - } - - /** - * @internal - */ - static OVERLAY_UNIFORMS = { - panorama : 'panorama', - overlay : 'overlay', - globalOpacity : 'globalOpacity', - overlayOpacity: 'overlayOpacity', - }; - - /** - * @internal - */ - static createOverlayMaterial({ additionalUniforms, overrideVertexShader } = {}) { - return new ShaderMaterial({ - uniforms: { - ...additionalUniforms, - [AbstractAdapter.OVERLAY_UNIFORMS.panorama] : { value: new Texture() }, - [AbstractAdapter.OVERLAY_UNIFORMS.overlay] : { value: new Texture() }, - [AbstractAdapter.OVERLAY_UNIFORMS.globalOpacity] : { value: 1.0 }, - [AbstractAdapter.OVERLAY_UNIFORMS.overlayOpacity]: { value: 1.0 }, - }, - - vertexShader: overrideVertexShader || ` -varying vec2 vUv; - -void main() { - vUv = uv; - gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); -}`, - - fragmentShader: ` -uniform sampler2D ${AbstractAdapter.OVERLAY_UNIFORMS.panorama}; -uniform sampler2D ${AbstractAdapter.OVERLAY_UNIFORMS.overlay}; -uniform float ${AbstractAdapter.OVERLAY_UNIFORMS.globalOpacity}; -uniform float ${AbstractAdapter.OVERLAY_UNIFORMS.overlayOpacity}; - -varying vec2 vUv; - -void main() { - vec4 tColor1 = texture2D( ${AbstractAdapter.OVERLAY_UNIFORMS.panorama}, vUv ); - vec4 tColor2 = texture2D( ${AbstractAdapter.OVERLAY_UNIFORMS.overlay}, vUv ); - gl_FragColor = vec4( - mix( tColor1.rgb, tColor2.rgb, tColor2.a * ${AbstractAdapter.OVERLAY_UNIFORMS.overlayOpacity} ), - ${AbstractAdapter.OVERLAY_UNIFORMS.globalOpacity} - ); -}`, - }); - } - -} diff --git a/src/adapters/cubemap-tiles/index.js b/src/adapters/cubemap-tiles/index.js deleted file mode 100644 index f2b674a2b..000000000 --- a/src/adapters/cubemap-tiles/index.js +++ /dev/null @@ -1,552 +0,0 @@ -import { - BoxGeometry, - Frustum, - ImageLoader, - MathUtils, - Matrix4, - Mesh, - MeshBasicMaterial, - Vector2, - Vector3 -} from 'three'; -import { CONSTANTS, PSVError, utils } from '../..'; -import { CUBE_HASHMAP, CubemapAdapter } from '../cubemap'; -import { Queue } from '../shared/Queue'; -import { Task } from '../shared/Task'; -import { buildErrorMaterial, createBaseTexture } from '../shared/tiles-utils'; - -if (!CubemapAdapter) { - throw new PSVError('CubemapAdapter is missing, please load cubemap.js before cubemap-tiles.js'); -} - - -/** - * @callback TileUrl - * @summary Function called to build a tile url - * @memberOf PSV.adapters.CubemapTilesAdapter - * @param {'left'|'front'|'right'|'back'|'top'|'bottom'} face - * @param {int} col - * @param {int} row - * @returns {string} - */ - -/** - * @typedef {Object} PSV.adapters.CubemapTilesAdapter.Panorama - * @summary Configuration of a tiled cubemap - * @property {PSV.adapters.CubemapAdapter.Cubemap} [baseUrl] - low resolution panorama loaded before tiles - * @property {int} faceSize - size of a face - * @property {int} nbTiles - number of tiles on a side of a face - * @property {PSV.adapters.CubemapTilesAdapter.TileUrl} tileUrl - function to build a tile url - */ - -/** - * @typedef {Object} PSV.adapters.CubemapTilesAdapter.Options - * @property {boolean} [flipTopBottom=false] - set to true if the top and bottom faces are not correctly oriented - * @property {boolean} [showErrorTile=true] - shows a warning sign on tiles that cannot be loaded - * @property {boolean} [baseBlur=true] - applies a blur to the low resolution panorama - */ - -/** - * @typedef {Object} PSV.adapters.CubemapTilesAdapter.Tile - * @private - * @property {int} face - * @property {int} col - * @property {int} row - * @property {float} angle - */ - - -const CUBE_SEGMENTS = 16; -const NB_VERTICES_BY_FACE = 6; -const NB_VERTICES_BY_PLANE = NB_VERTICES_BY_FACE * CUBE_SEGMENTS * CUBE_SEGMENTS; -const NB_VERTICES = 6 * NB_VERTICES_BY_PLANE; -const NB_GROUPS_BY_FACE = CUBE_SEGMENTS * CUBE_SEGMENTS; - -const ATTR_UV = 'uv'; -const ATTR_ORIGINAL_UV = 'originaluv'; -const ATTR_POSITION = 'position'; - -function tileId(tile) { - return `${tile.face}:${tile.col}x${tile.row}`; -} - -const frustum = new Frustum(); -const projScreenMatrix = new Matrix4(); -const vertexPosition = new Vector3(); - -/** - * @summary Adapter for tiled cubemaps - * @memberof PSV.adapters - * @extends PSV.adapters.AbstractAdapter - */ -export class CubemapTilesAdapter extends CubemapAdapter { - - static id = 'cubemap-tiles'; - static supportsDownload = false; - static supportsOverlay = false; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.adapters.CubemapTilesAdapter.Options} options - */ - constructor(psv, options) { - super(psv); - - /** - * @member {PSV.adapters.CubemapTilesAdapter.Options} - * @private - */ - this.config = { - flipTopBottom: false, - showErrorTile: true, - baseBlur : true, - ...options, - }; - - /** - * @member {PSV.adapters.Queue} - * @private - */ - this.queue = new Queue(); - - /** - * @type {Object} - * @property {int} tileSize - size in pixels of a tile - * @property {int} facesByTile - number of mesh faces by tile - * @property {Record} tiles - loaded tiles - * @property {external:THREE.BoxGeometry} geom - * @property {external:THREE.MeshBasicMaterial[]} materials - * @property {external:THREE.MeshBasicMaterial} errorMaterial - * @private - */ - this.prop = { - tileSize : 0, - facesByTile : 0, - tiles : {}, - geom : null, - materials : [], - errorMaterial: null, - }; - - /** - * @member {external:THREE.ImageLoader} - * @private - */ - this.loader = null; - - if (this.psv.config.requestHeaders) { - utils.logWarn('CubemapTilesAdapter fallbacks to file loader because "requestHeaders" where provided. ' - + 'Consider removing "requestHeaders" if you experience performances issues.'); - } - else { - this.loader = new ImageLoader(); - if (this.psv.config.withCredentials) { - this.loader.setWithCredentials(true); - } - } - - this.psv.on(CONSTANTS.EVENTS.POSITION_UPDATED, this); - this.psv.on(CONSTANTS.EVENTS.ZOOM_UPDATED, this); - } - - /** - * @override - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.POSITION_UPDATED, this); - this.psv.off(CONSTANTS.EVENTS.ZOOM_UPDATED, this); - - this.__cleanup(); - - this.prop.errorMaterial?.map?.dispose(); - this.prop.errorMaterial?.dispose(); - - delete this.queue; - delete this.loader; - delete this.prop.geom; - delete this.prop.errorMaterial; - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case CONSTANTS.EVENTS.POSITION_UPDATED: - case CONSTANTS.EVENTS.ZOOM_UPDATED: - this.__refresh(); - break; - } - /* eslint-enable */ - } - - /** - * @summary Clears loading queue, dispose all materials - * @private - */ - __cleanup() { - this.queue.clear(); - this.prop.tiles = {}; - - this.prop.materials.forEach((mat) => { - mat?.map?.dispose(); - mat?.dispose(); - }); - this.prop.materials.length = 0; - } - - /** - * @override - */ - supportsTransition(panorama) { - return !!panorama.baseUrl; - } - - /** - * @override - */ - supportsPreload(panorama) { - return !!panorama.baseUrl; - } - - /** - * @override - * @param {PSV.adapters.CubemapTilesAdapter.Panorama} panorama - * @returns {Promise.} - */ - loadTexture(panorama) { - if (typeof panorama !== 'object' || !panorama.faceSize || !panorama.nbTiles || !panorama.tileUrl) { - return Promise.reject(new PSVError('Invalid panorama configuration, are you using the right adapter?')); - } - if (panorama.nbTiles > CUBE_SEGMENTS) { - return Promise.reject(new PSVError(`Panorama nbTiles must not be greater than ${CUBE_SEGMENTS}.`)); - } - if (!MathUtils.isPowerOfTwo(panorama.nbTiles)) { - return Promise.reject(new PSVError('Panorama nbTiles must be power of 2.')); - } - - if (panorama.baseUrl) { - return super.loadTexture(panorama.baseUrl) - .then(textureData => ({ - panorama: panorama, - texture : textureData.texture, - })); - } - else { - return Promise.resolve({ panorama }); - } - } - - /** - * @override - */ - createMesh(scale = 1) { - const cubeSize = CONSTANTS.SPHERE_RADIUS * 2 * scale; - const geometry = new BoxGeometry(cubeSize, cubeSize, cubeSize, CUBE_SEGMENTS, CUBE_SEGMENTS, CUBE_SEGMENTS) - .scale(1, 1, -1) - .toNonIndexed(); - - geometry.clearGroups(); - for (let i = 0, k = 0; i < NB_VERTICES; i += NB_VERTICES_BY_FACE) { - geometry.addGroup(i, NB_VERTICES_BY_FACE, k++); - } - - geometry.setAttribute(ATTR_ORIGINAL_UV, geometry.getAttribute(ATTR_UV).clone()); - - return new Mesh(geometry, []); - } - - /** - * @summary Applies the base texture and starts the loading of tiles - * @override - */ - setTexture(mesh, textureData, transition) { - const { panorama, texture } = textureData; - - if (transition) { - this.__setTexture(mesh, texture); - return; - } - - this.__cleanup(); - this.__setTexture(mesh, texture); - - this.prop.materials = mesh.material; - this.prop.geom = mesh.geometry; - this.prop.geom.setAttribute(ATTR_UV, this.prop.geom.getAttribute(ATTR_ORIGINAL_UV).clone()); - - this.prop.tileSize = panorama.faceSize / panorama.nbTiles; - this.prop.facesByTile = CUBE_SEGMENTS / panorama.nbTiles; - - // this.psv.renderer.scene.add(createWireFrame(this.prop.geom)); - - setTimeout(() => this.__refresh(true)); - } - - /** - * @private - */ - __setTexture(mesh, texture) { - for (let i = 0; i < 6; i++) { - let material; - if (texture) { - if (this.config.flipTopBottom && (i === 2 || i === 3)) { - texture[i].center = new Vector2(0.5, 0.5); - texture[i].rotation = Math.PI; - } - - material = new MeshBasicMaterial({ map: texture[i] }); - } - else { - material = new MeshBasicMaterial({ opacity: 0, transparent: true }); - } - - for (let j = 0; j < NB_GROUPS_BY_FACE; j++) { - mesh.material.push(material); - } - } - } - - /** - * @override - */ - setTextureOpacity(mesh, opacity) { - for (let i = 0; i < 6; i++) { - mesh.material[i * NB_GROUPS_BY_FACE].opacity = opacity; - mesh.material[i * NB_GROUPS_BY_FACE].transparent = opacity < 1; - } - } - - /** - * @summary Compute visible tiles and load them - * @private - */ - __refresh(init = false) { // eslint-disable-line no-unused-vars - if (!this.prop.geom) { - return; - } - - const camera = this.psv.renderer.camera; - camera.updateMatrixWorld(); - projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); - frustum.setFromProjectionMatrix(projScreenMatrix); - - const panorama = this.psv.config.panorama; - const verticesPosition = this.prop.geom.getAttribute(ATTR_POSITION); - const tilesToLoad = []; - - for (let face = 0; face < 6; face++) { - for (let col = 0; col < panorama.nbTiles; col++) { - for (let row = 0; row < panorama.nbTiles; row++) { - // for each tile, find the vertices corresponding to the four corners - // if at least one vertex is visible, the tile must be loaded - // for larger tiles we also test the four edges centers and the tile center - const verticesIndex = []; - - // top-left - const v0 = face * NB_VERTICES_BY_PLANE - + row * this.prop.facesByTile * CUBE_SEGMENTS * NB_VERTICES_BY_FACE - + col * this.prop.facesByTile * NB_VERTICES_BY_FACE; - - // bottom-left - const v1 = v0 + CUBE_SEGMENTS * NB_VERTICES_BY_FACE * (this.prop.facesByTile - 1) + 1; - - // bottom-right - const v2 = v1 + this.prop.facesByTile * NB_VERTICES_BY_FACE - 3; - - // top-right - const v3 = v0 + this.prop.facesByTile * NB_VERTICES_BY_FACE - 1; - - verticesIndex.push(v0, v1, v2, v3); - - if (this.prop.facesByTile >= CUBE_SEGMENTS / 2) { - // top-center - const v4 = v0 + this.prop.facesByTile / 2 * NB_VERTICES_BY_FACE - 1; - - // bottom-center - const v5 = v1 + this.prop.facesByTile / 2 * NB_VERTICES_BY_FACE - 3; - - // left-center - const v6 = v0 + CUBE_SEGMENTS * NB_VERTICES_BY_FACE * (this.prop.facesByTile / 2 - 1) + 1; - - // right-center - const v7 = v6 + this.prop.facesByTile * NB_VERTICES_BY_FACE - 3; - - // center-center - const v8 = v6 + this.prop.facesByTile / 2 * NB_VERTICES_BY_FACE; - - verticesIndex.push(v4, v5, v6, v7, v8); - } - - // if (init && face === 5 && col === 0 && row === 0) { - // verticesIndex.forEach((vertexIdx) => { - // this.psv.renderer.scene.add(createDot( - // verticesPosition.getX(vertexIdx), - // verticesPosition.getY(vertexIdx), - // verticesPosition.getZ(vertexIdx) - // )); - // }); - // } - - const vertexVisible = verticesIndex.some((vertexIdx) => { - vertexPosition.set( - verticesPosition.getX(vertexIdx), - verticesPosition.getY(vertexIdx), - verticesPosition.getZ(vertexIdx) - ); - vertexPosition.applyEuler(this.psv.renderer.meshContainer.rotation); - return frustum.containsPoint(vertexPosition); - }); - - if (vertexVisible) { - const angle = vertexPosition.angleTo(this.psv.prop.direction); - tilesToLoad.push({ face, col, row, angle }); - } - } - } - } - - this.__loadTiles(tilesToLoad); - } - - /** - * @summary Loads tiles and change existing tiles priority - * @param {PSV.adapters.CubemapTilesAdapter.Tile[]} tiles - * @private - */ - __loadTiles(tiles) { - this.queue.disableAllTasks(); - - tiles.forEach((tile) => { - const id = tileId(tile); - - if (this.prop.tiles[id]) { - this.queue.setPriority(id, tile.angle); - } - else { - this.prop.tiles[id] = true; - this.queue.enqueue(new Task(id, tile.angle, task => this.__loadTile(tile, task))); - } - }); - - this.queue.start(); - } - - /** - * @summary Loads and draw a tile - * @param {PSV.adapters.CubemapTilesAdapter.Tile} tile - * @param {PSV.adapters.Task} task - * @return {Promise} - * @private - */ - __loadTile(tile, task) { - const panorama = this.psv.config.panorama; - - let { col, row } = tile; - if (this.config.flipTopBottom && (tile.face === 2 || tile.face === 3)) { - col = panorama.nbTiles - col - 1; - row = panorama.nbTiles - row - 1; - } - const url = panorama.tileUrl(CUBE_HASHMAP[tile.face], col, row); - - return this.__loadImage(url) - .then((image) => { - if (!task.isCancelled()) { - const material = new MeshBasicMaterial({ map: utils.createTexture(image) }); - this.__swapMaterial(tile.face, tile.col, tile.row, material); - this.psv.needsUpdate(); - } - }) - .catch(() => { - if (!task.isCancelled() && this.config.showErrorTile) { - if (!this.prop.errorMaterial) { - this.prop.errorMaterial = buildErrorMaterial(this.prop.tileSize, this.prop.tileSize); - } - this.__swapMaterial(tile.face, tile.col, tile.row, this.prop.errorMaterial); - this.psv.needsUpdate(); - } - }); - } - - /** - * @private - */ - __loadImage(url) { - if (this.loader) { - return new Promise((resolve, reject) => { - this.loader.load(url, resolve, undefined, reject); - }); - } - else { - return this.psv.textureLoader.loadImage(url); - } - } - - /** - * @summary Applies a new texture to the faces - * @param {int} face - * @param {int} col - * @param {int} row - * @param {external:THREE.MeshBasicMaterial} material - * @private - */ - __swapMaterial(face, col, row, material) { - const uvs = this.prop.geom.getAttribute(ATTR_UV); - - for (let c = 0; c < this.prop.facesByTile; c++) { - for (let r = 0; r < this.prop.facesByTile; r++) { - // position of the face (two triangles of the same square) - const faceCol = col * this.prop.facesByTile + c; - const faceRow = row * this.prop.facesByTile + r; - - // first vertex for this face (6 vertices in total) - const firstVertex = NB_VERTICES_BY_PLANE * face + 6 * (CUBE_SEGMENTS * faceRow + faceCol); - - // swap material - const matIndex = this.prop.geom.groups.find(g => g.start === firstVertex).materialIndex; - this.prop.materials[matIndex] = material; - - // define new uvs - let top = 1 - r / this.prop.facesByTile; - let bottom = 1 - (r + 1) / this.prop.facesByTile; - let left = c / this.prop.facesByTile; - let right = (c + 1) / this.prop.facesByTile; - - if (this.config.flipTopBottom && (face === 2 || face === 3)) { - top = 1 - top; - bottom = 1 - bottom; - left = 1 - left; - right = 1 - right; - } - - uvs.setXY(firstVertex, left, top); - uvs.setXY(firstVertex + 1, left, bottom); - uvs.setXY(firstVertex + 2, right, top); - uvs.setXY(firstVertex + 3, left, bottom); - uvs.setXY(firstVertex + 4, right, bottom); - uvs.setXY(firstVertex + 5, right, top); - } - } - - uvs.needsUpdate = true; - } - - /** - * @summary Create the texture for the base image - * @param {HTMLImageElement} img - * @return {external:THREE.Texture} - * @override - * @private - */ - __createCubemapTexture(img) { - if (img.width !== img.height) { - utils.logWarn('Invalid base image, the width should equals the height'); - } - - return createBaseTexture(img, this.config.baseBlur, w => w); - } - -} diff --git a/src/adapters/cubemap-video/index.js b/src/adapters/cubemap-video/index.js deleted file mode 100644 index 1a3e3f5db..000000000 --- a/src/adapters/cubemap-video/index.js +++ /dev/null @@ -1,186 +0,0 @@ -import { BoxGeometry, Mesh, ShaderMaterial, Vector2 } from 'three'; -import { CONSTANTS } from '../..'; -import { AbstractVideoAdapter } from '../shared/AbstractVideoAdapter'; - -/** - * @typedef {Object} PSV.adapters.CubemapVideoAdapter.Video - * @summary Object defining a video - * @property {string} source - */ - -/** - * @typedef {Object} PSV.adapters.CubemapVideoAdapter.Options - * @property {boolean} [autoplay=false] - automatically start the video - * @property {boolean} [muted=autoplay] - initially mute the video - * @property {number} [equiangular=true] - if the video is an equiangular cubemap (EAC) - */ - - -/** - * @summary Adapter for cubemap videos - * @memberof PSV.adapters - * @extends PSV.adapters.AbstractAdapter - */ -export class CubemapVideoAdapter extends AbstractVideoAdapter { - - static id = 'cubemap-video'; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.adapters.CubemapVideoAdapter.Options} options - */ - constructor(psv, options) { - super(psv, { - equiangular: true, - ...options, - }); - } - - /** - * @override - * @param {PSV.adapters.CubemapVideoAdapter.Video} panorama - * @returns {Promise.} - */ - loadTexture(panorama) { - return super.loadTexture(panorama); - } - - /** - * @override - */ - createMesh(scale = 1) { - const cubeSize = CONSTANTS.SPHERE_RADIUS * 2 * scale; - const geometry = new BoxGeometry(cubeSize, cubeSize, cubeSize) - .scale(1, 1, -1) - .toNonIndexed(); - - geometry.clearGroups(); - - const uvs = geometry.getAttribute('uv'); - - /* - Structure of a frame - - 1 +---------+---------+---------+ - | | | | - | Left | Front | Right | - | | | | - 1/2 +---------+---------+---------+ - | | | | - | Bottom | Back | Top | - | | | | - 0 +---------+---------+---------+ - 0 1/3 2/3 1 - - Bottom, Back and Top are rotated 90° clockwise - */ - - // columns - const a = 0; - const b = 1 / 3; - const c = 2 / 3; - const d = 1; - - // lines - const A = 1; - const B = 1 / 2; - const C = 0; - - // left - uvs.setXY(0, a, A); - uvs.setXY(1, a, B); - uvs.setXY(2, b, A); - uvs.setXY(3, a, B); - uvs.setXY(4, b, B); - uvs.setXY(5, b, A); - - // right - uvs.setXY(6, c, A); - uvs.setXY(7, c, B); - uvs.setXY(8, d, A); - uvs.setXY(9, c, B); - uvs.setXY(10, d, B); - uvs.setXY(11, d, A); - - // top - uvs.setXY(12, d, B); - uvs.setXY(13, c, B); - uvs.setXY(14, d, C); - uvs.setXY(15, c, B); - uvs.setXY(16, c, C); - uvs.setXY(17, d, C); - - // bottom - uvs.setXY(18, b, B); - uvs.setXY(19, a, B); - uvs.setXY(20, b, C); - uvs.setXY(21, a, B); - uvs.setXY(22, a, C); - uvs.setXY(23, b, C); - - // back - uvs.setXY(24, c, B); - uvs.setXY(25, b, B); - uvs.setXY(26, c, C); - uvs.setXY(27, b, B); - uvs.setXY(28, b, C); - uvs.setXY(29, c, C); - - // front - uvs.setXY(30, b, A); - uvs.setXY(31, b, B); - uvs.setXY(32, c, A); - uvs.setXY(33, b, B); - uvs.setXY(34, c, B); - uvs.setXY(35, c, A); - - // shamelessly copied from https://github.com/videojs/videojs-vr - const material = new ShaderMaterial({ - uniforms : { - mapped : { value: null }, - contCorrect: { value: 1 }, - faceWH : { value: new Vector2(1 / 3, 1 / 2) }, - vidWH : { value: new Vector2(1, 1) }, - }, - vertexShader : ` -varying vec2 vUv; -void main() { - vUv = uv; - gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.); -}`, - fragmentShader: ` -varying vec2 vUv; -uniform sampler2D mapped; -uniform vec2 faceWH; -uniform vec2 vidWH; -uniform float contCorrect; - -const float PI = 3.1415926535897932384626433832795; - -void main() { - vec2 corner = vUv - mod(vUv, faceWH) + vec2(0, contCorrect / vidWH.y); - vec2 faceWHadj = faceWH - vec2(0, contCorrect * 2. / vidWH.y); - vec2 p = (vUv - corner) / faceWHadj - .5; - vec2 q = ${this.config.equiangular ? '2. / PI * atan(2. * p) + .5' : 'p + .5'}; - vec2 eUv = corner + q * faceWHadj; - gl_FragColor = texture2D(mapped, eUv); -}`, - }); - - return new Mesh(geometry, material); - } - - /** - * @override - */ - setTexture(mesh, textureData) { - const { texture } = textureData; - - mesh.material.uniforms.mapped.value?.dispose(); - mesh.material.uniforms.mapped.value = texture; - mesh.material.uniforms.vidWH.value.set(texture.image.videoWidth, texture.image.videoHeight); - - this.__switchVideo(textureData.texture); - } - -} diff --git a/src/adapters/cubemap/index.js b/src/adapters/cubemap/index.js deleted file mode 100644 index 96b785f45..000000000 --- a/src/adapters/cubemap/index.js +++ /dev/null @@ -1,252 +0,0 @@ -import { BoxGeometry, Mesh, Texture } from 'three'; -import { AbstractAdapter, CONSTANTS, PSVError, SYSTEM, utils } from '../..'; - - -/** - * @typedef {Object} PSV.adapters.CubemapAdapter.Cubemap - * @summary Object defining a cubemap - * @property {string} left - * @property {string} front - * @property {string} right - * @property {string} back - * @property {string} top - * @property {string} bottom - */ - -/** - * @typedef {Object} PSV.adapters.CubemapAdapter.Options - * @property {boolean} [flipTopBottom=false] - set to true if the top and bottom faces are not correctly oriented - */ - - -// PSV faces order is left, front, right, back, top, bottom -// 3JS faces order is left, right, top, bottom, back, front -export const CUBE_ARRAY = [0, 2, 4, 5, 3, 1]; -export const CUBE_HASHMAP = ['left', 'right', 'top', 'bottom', 'back', 'front']; - - -/** - * @summary Adapter for cubemaps - * @memberof PSV.adapters - * @extends PSV.adapters.AbstractAdapter - */ -export class CubemapAdapter extends AbstractAdapter { - - static id = 'cubemap'; - static supportsDownload = false; - static supportsOverlay = true; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.adapters.CubemapAdapter.Options} options - */ - constructor(psv, options) { - super(psv); - - /** - * @member {PSV.adapters.CubemapAdapter.Options} - * @private - */ - this.config = { - flipTopBottom: false, - ...options, - }; - } - - /** - * @override - */ - supportsTransition() { - return true; - } - - /** - * @override - */ - supportsPreload() { - return true; - } - - /** - * @override - * @param {string[] | PSV.adapters.CubemapAdapter.Cubemap} panorama - * @returns {Promise.} - */ - loadTexture(panorama) { - const cleanPanorama = []; - - if (Array.isArray(panorama)) { - if (panorama.length !== 6) { - return Promise.reject(new PSVError('Must provide exactly 6 image paths when using cubemap.')); - } - - // reorder images - for (let i = 0; i < 6; i++) { - cleanPanorama[i] = panorama[CUBE_ARRAY[i]]; - } - } - else if (typeof panorama === 'object') { - if (!CUBE_HASHMAP.every(side => !!panorama[side])) { - return Promise.reject(new PSVError('Must provide exactly left, front, right, back, top, bottom when using cubemap.')); - } - - // transform into array - CUBE_HASHMAP.forEach((side, i) => { - cleanPanorama[i] = panorama[side]; - }); - } - else { - return Promise.reject(new PSVError('Invalid cubemap panorama, are you using the right adapter?')); - } - - if (this.psv.config.fisheye) { - utils.logWarn('fisheye effect with cubemap texture can generate distorsion'); - } - - const promises = []; - const progress = [0, 0, 0, 0, 0, 0]; - - for (let i = 0; i < 6; i++) { - promises.push( - this.psv.textureLoader.loadImage(cleanPanorama[i], (p) => { - progress[i] = p; - this.psv.loader.setProgress(utils.sum(progress) / 6); - }) - .then(img => this.__createCubemapTexture(img)) - ); - } - - return Promise.all(promises) - .then(texture => ({ panorama, texture })); - } - - /** - * @summary Creates the final texture from image - * @param {HTMLImageElement} img - * @returns {external:THREE.Texture} - * @private - */ - __createCubemapTexture(img) { - if (img.width !== img.height) { - utils.logWarn('Invalid base image, the width equal the height'); - } - - // resize image - if (img.width > SYSTEM.maxTextureWidth) { - const ratio = SYSTEM.getMaxCanvasWidth() / img.width; - - const buffer = document.createElement('canvas'); - buffer.width = img.width * ratio; - buffer.height = img.height * ratio; - - const ctx = buffer.getContext('2d'); - ctx.drawImage(img, 0, 0, buffer.width, buffer.height); - - return utils.createTexture(buffer); - } - - return utils.createTexture(img); - } - - /** - * @override - */ - createMesh(scale = 1) { - const cubeSize = CONSTANTS.SPHERE_RADIUS * 2 * scale; - const geometry = new BoxGeometry(cubeSize, cubeSize, cubeSize) - .scale(1, 1, -1); - - const materials = []; - for (let i = 0; i < 6; i++) { - materials.push(AbstractAdapter.createOverlayMaterial({ - additionalUniforms: { - rotation: { value: 0.0 }, - }, - overrideVertexShader: ` -uniform float rotation; - -varying vec2 vUv; - -const float mid = 0.5; - -void main() { - if (rotation == 0.0) { - vUv = uv; - } else { - vUv = vec2( - cos(rotation) * (uv.x - mid) + sin(rotation) * (uv.y - mid) + mid, - cos(rotation) * (uv.y - mid) - sin(rotation) * (uv.x - mid) + mid - ); - } - gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); -}`, - })); - } - - return new Mesh(geometry, materials); - } - - /** - * @override - */ - setTexture(mesh, textureData) { - const { texture } = textureData; - - for (let i = 0; i < 6; i++) { - if (this.config.flipTopBottom && (i === 2 || i === 3)) { - this.__setUniform(mesh, i, 'rotation', Math.PI); - } - - this.__setUniform(mesh, i, AbstractAdapter.OVERLAY_UNIFORMS.panorama, texture[i]); - } - - this.setOverlay(mesh, null); - } - - /** - * @override - */ - setOverlay(mesh, textureData, opacity) { - for (let i = 0; i < 6; i++) { - this.__setUniform(mesh, i, AbstractAdapter.OVERLAY_UNIFORMS.overlayOpacity, opacity); - if (!textureData) { - this.__setUniform(mesh, i, AbstractAdapter.OVERLAY_UNIFORMS.overlay, new Texture()); - } - else { - this.__setUniform(mesh, i, AbstractAdapter.OVERLAY_UNIFORMS.overlay, textureData.texture[i]); - } - } - } - - /** - * @override - */ - setTextureOpacity(mesh, opacity) { - for (let i = 0; i < 6; i++) { - this.__setUniform(mesh, i, AbstractAdapter.OVERLAY_UNIFORMS.globalOpacity, opacity); - mesh.material[i].transparent = opacity < 1; - } - } - - /** - * @override - */ - disposeTexture(textureData) { - textureData.texture?.forEach(texture => texture.dispose()); - } - - /** - * @param {external:THREE.Mesh} mesh - * @param {number} index - * @param {string} uniform - * @param {*} value - * @private - */ - __setUniform(mesh, index, uniform, value) { - if (mesh.material[index].uniforms[uniform].value instanceof Texture) { - mesh.material[index].uniforms[uniform].value.dispose(); - } - mesh.material[index].uniforms[uniform].value = value; - } - -} diff --git a/src/adapters/equirectangular-tiles/index.js b/src/adapters/equirectangular-tiles/index.js deleted file mode 100644 index e7504f566..000000000 --- a/src/adapters/equirectangular-tiles/index.js +++ /dev/null @@ -1,702 +0,0 @@ -import { Frustum, ImageLoader, MathUtils, Matrix4, Mesh, MeshBasicMaterial, SphereGeometry, Vector3 } from 'three'; -import { CONSTANTS, EquirectangularAdapter, PSVError, utils } from '../..'; -import { Queue } from '../shared/Queue'; -import { Task } from '../shared/Task'; -import { buildErrorMaterial, createBaseTexture } from '../shared/tiles-utils'; - - -/** - * @callback TileUrl - * @summary Function called to build a tile url - * @memberOf PSV.adapters.EquirectangularTilesAdapter - * @param {int} col - * @param {int} row - * @returns {string} - */ - -/** - * @typedef {Object} PSV.adapters.EquirectangularTilesAdapter.Panorama - * @summary Configuration of a tiled panorama - * @property {string} [baseUrl] - low resolution panorama loaded before tiles - * @property {PSV.PanoData | PSV.PanoDataProvider} [basePanoData] - panoData configuration associated to low resolution panorama loaded before tiles - * @property {int} width - complete panorama width (height is always width/2) - * @property {int} cols - number of vertical tiles - * @property {int} rows - number of horizontal tiles - * @property {PSV.adapters.EquirectangularTilesAdapter.TileUrl} tileUrl - function to build a tile url - */ - -/** - * @typedef {Object} PSV.adapters.EquirectangularTilesAdapter.Options - * @property {number} [resolution=64] - number of faces of the sphere geometry, higher values may decrease performances - * @property {boolean} [showErrorTile=true] - shows a warning sign on tiles that cannot be loaded - * @property {boolean} [baseBlur=true] - applies a blur to the low resolution panorama - */ - -/** - * @typedef {Object} PSV.adapters.EquirectangularTilesAdapter.Tile - * @private - * @property {int} col - * @property {int} row - * @property {float} angle - */ - -/* the faces of the top and bottom rows are made of a single triangle (3 vertices) - * all other faces are made of two triangles (6 vertices) - * bellow is the indexing of each face vertices - * - * first row faces: - * ⋀ - * /0\ - * / \ - * / \ - * /1 2\ - * ¯¯¯¯¯¯¯¯¯ - * - * other rows faces: - * _________ - * |\1 0| - * |3\ | - * | \ | - * | \ | - * | \ | - * | \2| - * |4 5\| - * ¯¯¯¯¯¯¯¯¯ - * - * last row faces: - * _________ - * \1 0/ - * \ / - * \ / - * \2/ - * ⋁ - */ - -const ATTR_UV = 'uv'; -const ATTR_ORIGINAL_UV = 'originaluv'; -const ATTR_POSITION = 'position'; - -function tileId(tile) { - return `${tile.col}x${tile.row}`; -} - -const frustum = new Frustum(); -const projScreenMatrix = new Matrix4(); -const vertexPosition = new Vector3(); - - -/** - * @summary Adapter for tiled panoramas - * @memberof PSV.adapters - * @extends PSV.adapters.AbstractAdapter - */ -export class EquirectangularTilesAdapter extends EquirectangularAdapter { - - static id = 'equirectangular-tiles'; - static supportsDownload = false; - static supportsOverlay = false; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.adapters.EquirectangularTilesAdapter.Options} options - */ - constructor(psv, options) { - super(psv); - - this.psv.config.useXmpData = false; - - /** - * @member {PSV.adapters.EquirectangularTilesAdapter.Options} - * @private - */ - this.config = { - resolution : 64, - showErrorTile: true, - baseBlur : true, - ...options, - }; - - if (!MathUtils.isPowerOfTwo(this.config.resolution)) { - throw new PSVError('EquirectangularAdapter resolution must be power of two'); - } - - this.SPHERE_SEGMENTS = this.config.resolution; - this.SPHERE_HORIZONTAL_SEGMENTS = this.SPHERE_SEGMENTS / 2; - this.NB_VERTICES_BY_FACE = 6; - this.NB_VERTICES_BY_SMALL_FACE = 3; - this.NB_VERTICES = 2 * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_SMALL_FACE - + (this.SPHERE_HORIZONTAL_SEGMENTS - 2) * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE; - this.NB_GROUPS = this.SPHERE_SEGMENTS * this.SPHERE_HORIZONTAL_SEGMENTS; - - /** - * @member {PSV.adapters.Queue} - * @private - */ - this.queue = new Queue(); - - /** - * @type {Object} - * @property {int} colSize - size in pixels of a column - * @property {int} rowSize - size in pixels of a row - * @property {int} facesByCol - number of mesh faces by column - * @property {int} facesByRow - number of mesh faces by row - * @property {Record} tiles - loaded tiles - * @property {external:THREE.SphereGeometry} geom - * @property {external:THREE.MeshBasicMaterial[]} materials - * @property {external:THREE.MeshBasicMaterial} errorMaterial - * @private - */ - this.prop = { - colSize : 0, - rowSize : 0, - facesByCol : 0, - facesByRow : 0, - tiles : {}, - geom : null, - materials : [], - errorMaterial: null, - }; - - /** - * @member {external:THREE.ImageLoader} - * @private - */ - this.loader = null; - - if (this.psv.config.requestHeaders) { - utils.logWarn('EquirectangularTilesAdapter fallbacks to file loader because "requestHeaders" where provided. ' - + 'Consider removing "requestHeaders" if you experience performances issues.'); - } - else { - this.loader = new ImageLoader(); - if (this.psv.config.withCredentials) { - this.loader.setWithCredentials(true); - } - } - - this.psv.on(CONSTANTS.EVENTS.POSITION_UPDATED, this); - this.psv.on(CONSTANTS.EVENTS.ZOOM_UPDATED, this); - } - - /** - * @override - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.POSITION_UPDATED, this); - this.psv.off(CONSTANTS.EVENTS.ZOOM_UPDATED, this); - - this.__cleanup(); - - this.prop.errorMaterial?.map?.dispose(); - this.prop.errorMaterial?.dispose(); - - delete this.queue; - delete this.loader; - delete this.prop.geom; - delete this.prop.errorMaterial; - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case CONSTANTS.EVENTS.POSITION_UPDATED: - case CONSTANTS.EVENTS.ZOOM_UPDATED: - this.__refresh(); - break; - } - /* eslint-enable */ - } - - /** - * @summary Clears loading queue, dispose all materials - * @private - */ - __cleanup() { - this.queue.clear(); - this.prop.tiles = {}; - - this.prop.materials.forEach((mat) => { - mat?.map?.dispose(); - mat?.dispose(); - }); - this.prop.materials.length = 0; - } - - /** - * @override - */ - supportsTransition(panorama) { - return !!panorama.baseUrl; - } - - /** - * @override - */ - supportsPreload(panorama) { - return !!panorama.baseUrl; - } - - /** - * @override - * @param {PSV.adapters.EquirectangularTilesAdapter.Panorama} panorama - * @returns {Promise.} - */ - loadTexture(panorama) { - if (typeof panorama !== 'object' || !panorama.width || !panorama.cols || !panorama.rows || !panorama.tileUrl) { - return Promise.reject(new PSVError('Invalid panorama configuration, are you using the right adapter?')); - } - if (panorama.cols > this.SPHERE_SEGMENTS) { - return Promise.reject(new PSVError(`Panorama cols must not be greater than ${this.SPHERE_SEGMENTS}.`)); - } - if (panorama.rows > this.SPHERE_HORIZONTAL_SEGMENTS) { - return Promise.reject(new PSVError(`Panorama rows must not be greater than ${this.SPHERE_HORIZONTAL_SEGMENTS}.`)); - } - if (!MathUtils.isPowerOfTwo(panorama.cols) || !MathUtils.isPowerOfTwo(panorama.rows)) { - return Promise.reject(new PSVError('Panorama cols and rows must be powers of 2.')); - } - - const panoData = { - fullWidth : panorama.width, - fullHeight : panorama.width / 2, - croppedWidth : panorama.width, - croppedHeight: panorama.width / 2, - croppedX : 0, - croppedY : 0, - poseHeading : 0, - posePitch : 0, - poseRoll : 0, - }; - - if (panorama.baseUrl) { - return super.loadTexture(panorama.baseUrl, panorama.basePanoData) - .then(textureData => ({ - panorama: panorama, - texture : textureData.texture, - panoData: panoData, - })); - } - else { - return Promise.resolve({ panorama, panoData }); - } - } - - /** - * @override - */ - createMesh(scale = 1) { - const geometry = new SphereGeometry( - CONSTANTS.SPHERE_RADIUS * scale, - this.SPHERE_SEGMENTS, - this.SPHERE_HORIZONTAL_SEGMENTS, - -Math.PI / 2 - ) - .scale(-1, 1, 1) - .toNonIndexed(); - - geometry.clearGroups(); - let i = 0; - let k = 0; - // first row - for (; i < this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_SMALL_FACE; i += this.NB_VERTICES_BY_SMALL_FACE) { - geometry.addGroup(i, this.NB_VERTICES_BY_SMALL_FACE, k++); - } - // second to before last rows - for (; i < this.NB_VERTICES - this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_SMALL_FACE; i += this.NB_VERTICES_BY_FACE) { - geometry.addGroup(i, this.NB_VERTICES_BY_FACE, k++); - } - // last row - for (; i < this.NB_VERTICES; i += this.NB_VERTICES_BY_SMALL_FACE) { - geometry.addGroup(i, this.NB_VERTICES_BY_SMALL_FACE, k++); - } - - geometry.setAttribute(ATTR_ORIGINAL_UV, geometry.getAttribute(ATTR_UV).clone()); - - return new Mesh(geometry, []); - } - - /** - * @summary Applies the base texture and starts the loading of tiles - * @override - */ - setTexture(mesh, textureData, transition) { - const { panorama, texture } = textureData; - - if (transition) { - this.__setTexture(mesh, texture); - return; - } - - this.__cleanup(); - this.__setTexture(mesh, texture); - - this.prop.materials = mesh.material; - this.prop.geom = mesh.geometry; - this.prop.geom.setAttribute(ATTR_UV, this.prop.geom.getAttribute(ATTR_ORIGINAL_UV).clone()); - - this.prop.colSize = panorama.width / panorama.cols; - this.prop.rowSize = panorama.width / 2 / panorama.rows; - this.prop.facesByCol = this.SPHERE_SEGMENTS / panorama.cols; - this.prop.facesByRow = this.SPHERE_HORIZONTAL_SEGMENTS / panorama.rows; - - // this.psv.renderer.scene.add(createWireFrame(this.prop.geom)); - - setTimeout(() => this.__refresh(true)); - } - - /** - * @private - */ - __setTexture(mesh, texture) { - let material; - if (texture) { - material = new MeshBasicMaterial({ map: texture }); - } - else { - material = new MeshBasicMaterial({ opacity: 0, transparent: true }); - } - - for (let i = 0; i < this.NB_GROUPS; i++) { - mesh.material.push(material); - } - } - - /** - * @override - */ - setTextureOpacity(mesh, opacity) { - mesh.material[0].opacity = opacity; - mesh.material[0].transparent = opacity < 1; - } - - /** - * @summary Compute visible tiles and load them - * @param {boolean} [init=false] Indicates initial call - * @private - */ - __refresh(init = false) { // eslint-disable-line no-unused-vars - if (!this.prop.geom) { - return; - } - - const camera = this.psv.renderer.camera; - camera.updateMatrixWorld(); - projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); - frustum.setFromProjectionMatrix(projScreenMatrix); - - const panorama = this.psv.config.panorama; - const verticesPosition = this.prop.geom.getAttribute(ATTR_POSITION); - const tilesToLoad = []; - - for (let col = 0; col < panorama.cols; col++) { - for (let row = 0; row < panorama.rows; row++) { - // for each tile, find the vertices corresponding to the four corners (three for first and last rows) - // if at least one vertex is visible, the tile must be loaded - // for larger tiles we also test the four edges centers and the tile center - - const verticesIndex = []; - - if (row === 0) { - // bottom-left - const v0 = this.prop.facesByRow === 1 - ? col * this.prop.facesByCol * this.NB_VERTICES_BY_SMALL_FACE + 1 - : this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_SMALL_FACE - + (this.prop.facesByRow - 2) * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE - + col * this.prop.facesByCol * this.NB_VERTICES_BY_FACE + 4; - - // bottom-right - const v1 = this.prop.facesByRow === 1 - ? v0 + (this.prop.facesByCol - 1) * this.NB_VERTICES_BY_SMALL_FACE + 1 - : v0 + (this.prop.facesByCol - 1) * this.NB_VERTICES_BY_FACE + 1; - - // top (all vertices are equal) - const v2 = 0; - - verticesIndex.push(v0, v1, v2); - - if (this.prop.facesByCol >= this.SPHERE_SEGMENTS / 8) { - // bottom-center - const v4 = v0 + this.prop.facesByCol / 2 * this.NB_VERTICES_BY_FACE; - - verticesIndex.push(v4); - } - - if (this.prop.facesByRow >= this.SPHERE_HORIZONTAL_SEGMENTS / 4) { - // left-center - const v6 = v0 - this.prop.facesByRow / 2 * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE; - - // right-center - const v7 = v1 - this.prop.facesByRow / 2 * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE; - - verticesIndex.push(v6, v7); - } - } - else if (row === panorama.rows - 1) { - // top-left - const v0 = this.prop.facesByRow === 1 - ? -this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_SMALL_FACE - + row * this.prop.facesByRow * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE - + col * this.prop.facesByCol * this.NB_VERTICES_BY_SMALL_FACE + 1 - : -this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_SMALL_FACE - + row * this.prop.facesByRow * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE - + col * this.prop.facesByCol * this.NB_VERTICES_BY_FACE + 1; - - // top-right - const v1 = this.prop.facesByRow === 1 - ? v0 + (this.prop.facesByCol - 1) * this.NB_VERTICES_BY_SMALL_FACE - 1 - : v0 + (this.prop.facesByCol - 1) * this.NB_VERTICES_BY_FACE - 1; - - // bottom (all vertices are equal) - const v2 = this.NB_VERTICES - 1; - - verticesIndex.push(v0, v1, v2); - - if (this.prop.facesByCol >= this.SPHERE_SEGMENTS / 8) { - // top-center - const v4 = v0 + this.prop.facesByCol / 2 * this.NB_VERTICES_BY_FACE; - - verticesIndex.push(v4); - } - - if (this.prop.facesByRow >= this.SPHERE_HORIZONTAL_SEGMENTS / 4) { - // left-center - const v6 = v0 + this.prop.facesByRow / 2 * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE; - - // right-center - const v7 = v1 + this.prop.facesByRow / 2 * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE; - - verticesIndex.push(v6, v7); - } - } - else { - // top-left - const v0 = -this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_SMALL_FACE - + row * this.prop.facesByRow * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE - + col * this.prop.facesByCol * this.NB_VERTICES_BY_FACE + 1; - - // bottom-left - const v1 = v0 + (this.prop.facesByRow - 1) * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE + 3; - - // bottom-right - const v2 = v1 + (this.prop.facesByCol - 1) * this.NB_VERTICES_BY_FACE + 1; - - // top-right - const v3 = v0 + (this.prop.facesByCol - 1) * this.NB_VERTICES_BY_FACE - 1; - - verticesIndex.push(v0, v1, v2, v3); - - if (this.prop.facesByCol >= this.SPHERE_SEGMENTS / 8) { - // top-center - const v4 = v0 + this.prop.facesByCol / 2 * this.NB_VERTICES_BY_FACE; - - // bottom-center - const v5 = v1 + this.prop.facesByCol / 2 * this.NB_VERTICES_BY_FACE; - - verticesIndex.push(v4, v5); - } - - if (this.prop.facesByRow >= this.SPHERE_HORIZONTAL_SEGMENTS / 4) { - // left-center - const v6 = v0 + this.prop.facesByRow / 2 * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE; - - // right-center - const v7 = v3 + this.prop.facesByRow / 2 * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE; - - verticesIndex.push(v6, v7); - - if (this.prop.facesByCol >= this.SPHERE_SEGMENTS / 8) { - // center-center - const v8 = v6 + this.prop.facesByCol / 2 * this.NB_VERTICES_BY_FACE; - - verticesIndex.push(v8); - } - } - } - - // if (init && col === 0 && row === 0) { - // verticesIndex.forEach((vertexIdx) => { - // this.psv.renderer.scene.add(createDot( - // verticesPosition.getX(vertexIdx), - // verticesPosition.getY(vertexIdx), - // verticesPosition.getZ(vertexIdx) - // )); - // }); - // } - - const vertexVisible = verticesIndex.some((vertexIdx) => { - vertexPosition.set( - verticesPosition.getX(vertexIdx), - verticesPosition.getY(vertexIdx), - verticesPosition.getZ(vertexIdx) - ); - vertexPosition.applyEuler(this.psv.renderer.meshContainer.rotation); - return frustum.containsPoint(vertexPosition); - }); - - if (vertexVisible) { - let angle = vertexPosition.angleTo(this.psv.prop.direction); - if (row === 0 || row === panorama.rows - 1) { - angle *= 2; // lower priority to top and bottom tiles - } - tilesToLoad.push({ col, row, angle }); - } - } - } - - this.__loadTiles(tilesToLoad); - } - - /** - * @summary Loads tiles and change existing tiles priority - * @param {PSV.adapters.EquirectangularTilesAdapter.Tile[]} tiles - * @private - */ - __loadTiles(tiles) { - this.queue.disableAllTasks(); - - tiles.forEach((tile) => { - const id = tileId(tile); - - if (this.prop.tiles[id]) { - this.queue.setPriority(id, tile.angle); - } - else { - this.prop.tiles[id] = true; - this.queue.enqueue(new Task(id, tile.angle, task => this.__loadTile(tile, task))); - } - }); - - this.queue.start(); - } - - /** - * @summary Loads and draw a tile - * @param {PSV.adapters.EquirectangularTilesAdapter.Tile} tile - * @param {PSV.adapters.Task} task - * @return {Promise} - * @private - */ - __loadTile(tile, task) { - const panorama = this.psv.config.panorama; - const url = panorama.tileUrl(tile.col, tile.row); - - return this.__loadImage(url) - .then((image) => { - if (!task.isCancelled()) { - const material = new MeshBasicMaterial({ map: utils.createTexture(image) }); - this.__swapMaterial(tile.col, tile.row, material); - this.psv.needsUpdate(); - } - }) - .catch(() => { - if (!task.isCancelled() && this.config.showErrorTile) { - if (!this.prop.errorMaterial) { - this.prop.errorMaterial = buildErrorMaterial(this.prop.colSize, this.prop.rowSize); - } - this.__swapMaterial(tile.col, tile.row, this.prop.errorMaterial); - this.psv.needsUpdate(); - } - }); - } - - /** - * @private - */ - __loadImage(url) { - if (this.loader) { - return new Promise((resolve, reject) => { - this.loader.load(url, resolve, undefined, reject); - }); - } - else { - return this.psv.textureLoader.loadImage(url); - } - } - - /** - * @summary Applies a new texture to the faces - * @param {int} col - * @param {int} row - * @param {external:THREE.MeshBasicMaterial} material - * @private - */ - __swapMaterial(col, row, material) { - const uvs = this.prop.geom.getAttribute(ATTR_UV); - - for (let c = 0; c < this.prop.facesByCol; c++) { - for (let r = 0; r < this.prop.facesByRow; r++) { - // position of the face (two triangles of the same square) - const faceCol = col * this.prop.facesByCol + c; - const faceRow = row * this.prop.facesByRow + r; - const isFirstRow = faceRow === 0; - const isLastRow = faceRow === (this.SPHERE_HORIZONTAL_SEGMENTS - 1); - - // first vertex for this face (3 or 6 vertices in total) - let firstVertex; - if (isFirstRow) { - firstVertex = faceCol * this.NB_VERTICES_BY_SMALL_FACE; - } - else if (isLastRow) { - firstVertex = this.NB_VERTICES - - this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_SMALL_FACE - + faceCol * this.NB_VERTICES_BY_SMALL_FACE; - } - else { - firstVertex = this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_SMALL_FACE - + (faceRow - 1) * this.SPHERE_SEGMENTS * this.NB_VERTICES_BY_FACE - + faceCol * this.NB_VERTICES_BY_FACE; - } - - // swap material - const matIndex = this.prop.geom.groups.find(g => g.start === firstVertex).materialIndex; - this.prop.materials[matIndex] = material; - - // define new uvs - const top = 1 - r / this.prop.facesByRow; - const bottom = 1 - (r + 1) / this.prop.facesByRow; - const left = c / this.prop.facesByCol; - const right = (c + 1) / this.prop.facesByCol; - - if (isFirstRow) { - uvs.setXY(firstVertex, (left + right) / 2, top); - uvs.setXY(firstVertex + 1, left, bottom); - uvs.setXY(firstVertex + 2, right, bottom); - } - else if (isLastRow) { - uvs.setXY(firstVertex, right, top); - uvs.setXY(firstVertex + 1, left, top); - uvs.setXY(firstVertex + 2, (left + right) / 2, bottom); - } - else { - uvs.setXY(firstVertex, right, top); - uvs.setXY(firstVertex + 1, left, top); - uvs.setXY(firstVertex + 2, right, bottom); - uvs.setXY(firstVertex + 3, left, top); - uvs.setXY(firstVertex + 4, left, bottom); - uvs.setXY(firstVertex + 5, right, bottom); - } - } - } - - uvs.needsUpdate = true; - } - - /** - * @summary Create the texture for the base image - * @param {HTMLImageElement} img - * @return {external:THREE.Texture} - * @private - */ - __createBaseTexture(img) { - if (img.width !== img.height * 2) { - utils.logWarn('Invalid base image, the width should be twice the height'); - } - - return createBaseTexture(img, this.config.baseBlur, w => w / 2); - } - -} diff --git a/src/adapters/equirectangular-video/index.js b/src/adapters/equirectangular-video/index.js deleted file mode 100644 index b4dcd29c0..000000000 --- a/src/adapters/equirectangular-video/index.js +++ /dev/null @@ -1,98 +0,0 @@ -import { MathUtils, Mesh, MeshBasicMaterial, SphereGeometry } from 'three'; -import { CONSTANTS, PSVError } from '../..'; -import { AbstractVideoAdapter } from '../shared/AbstractVideoAdapter'; - -/** - * @typedef {Object} PSV.adapters.EquirectangularVideoAdapter.Video - * @summary Object defining a video - * @property {string} source - */ - -/** - * @typedef {Object} PSV.adapters.EquirectangularVideoAdapter.Options - * @property {boolean} [autoplay=false] - automatically start the video - * @property {boolean} [muted=autoplay] - initially mute the video - * @property {number} [resolution=64] - number of faces of the sphere geometry, higher values may decrease performances - */ - - -/** - * @summary Adapter for equirectangular videos - * @memberof PSV.adapters - * @extends PSV.adapters.AbstractAdapter - */ -export class EquirectangularVideoAdapter extends AbstractVideoAdapter { - - static id = 'equirectangular-video'; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.adapters.EquirectangularVideoAdapter.Options} options - */ - constructor(psv, options) { - super(psv, { - resolution: 64, - ...options, - }); - - if (!MathUtils.isPowerOfTwo(this.config.resolution)) { - throw new PSVError('EquirectangularVideoAdapter resolution must be power of two'); - } - - this.SPHERE_SEGMENTS = this.config.resolution; - this.SPHERE_HORIZONTAL_SEGMENTS = this.SPHERE_SEGMENTS / 2; - } - - /** - * @override - * @param {PSV.adapters.EquirectangularVideoAdapter.Video} panorama - * @returns {Promise.} - */ - loadTexture(panorama) { - return super.loadTexture(panorama) - .then(({ texture }) => { - const video = texture.image; - const panoData = { - fullWidth : video.videoWidth, - fullHeight : video.videoHeight, - croppedWidth : video.videoWidth, - croppedHeight: video.videoHeight, - croppedX : 0, - croppedY : 0, - poseHeading : 0, - posePitch : 0, - poseRoll : 0, - }; - - return { panorama, texture, panoData }; - }); - } - - /** - * @override - */ - createMesh(scale = 1) { - const geometry = new SphereGeometry( - CONSTANTS.SPHERE_RADIUS * scale, - this.SPHERE_SEGMENTS, - this.SPHERE_HORIZONTAL_SEGMENTS, - -Math.PI / 2 - ) - .scale(-1, 1, 1); - - const material = new MeshBasicMaterial(); - - return new Mesh(geometry, material); - } - - /** - * @override - */ - setTexture(mesh, textureData) { - mesh.material.map?.dispose(); - mesh.material.map = textureData.texture; - - this.__switchVideo(textureData.texture); - } - -} diff --git a/src/adapters/equirectangular/index.js b/src/adapters/equirectangular/index.js deleted file mode 100644 index 2cda7570e..000000000 --- a/src/adapters/equirectangular/index.js +++ /dev/null @@ -1,273 +0,0 @@ -import { MathUtils, Mesh, SphereGeometry, Texture } from 'three'; -import { SPHERE_RADIUS } from '../../data/constants'; -import { SYSTEM } from '../../data/system'; -import { PSVError } from '../../PSVError'; -import { createTexture, firstNonNull, getXMPValue, logWarn } from '../../utils'; -import { AbstractAdapter } from '../AbstractAdapter'; - - -/** - * @typedef {Object} PSV.adapters.EquirectangularAdapter.Options - * @property {number} [resolution=64] - number of faces of the sphere geometry, higher values may decrease performances - */ - - -/** - * @summary Adapter for equirectangular panoramas - * @memberof PSV.adapters - * @extends PSV.adapters.AbstractAdapter - */ -export class EquirectangularAdapter extends AbstractAdapter { - - static id = 'equirectangular'; - static supportsDownload = true; - static supportsOverlay = true; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.adapters.EquirectangularAdapter.Options} options - */ - constructor(psv, options) { - super(psv); - - /** - * @member {PSV.adapters.EquirectangularAdapter.Options} - * @private - */ - this.config = { - resolution: 64, - ...options, - }; - - if (!MathUtils.isPowerOfTwo(this.config.resolution)) { - throw new PSVError('EquirectangularAdapter resolution must be power of two'); - } - - this.SPHERE_SEGMENTS = this.config.resolution; - this.SPHERE_HORIZONTAL_SEGMENTS = this.SPHERE_SEGMENTS / 2; - } - - /** - * @override - */ - supportsTransition() { - return true; - } - - /** - * @override - */ - supportsPreload() { - return true; - } - - /** - * @override - * @param {string} panorama - * @param {PSV.PanoData | PSV.PanoDataProvider} [newPanoData] - * @param {boolean} [useXmpPanoData] - * @returns {Promise.} - */ - loadTexture(panorama, newPanoData, useXmpPanoData = this.psv.config.useXmpData) { - if (typeof panorama !== 'string') { - if (Array.isArray(panorama) || typeof panorama === 'object' && !!panorama.left) { - logWarn('Cubemap support now requires an additional adapter, see https://photo-sphere-viewer.js.org/guide/adapters'); - } - return Promise.reject(new PSVError('Invalid panorama url, are you using the right adapter?')); - } - - return ( - useXmpPanoData - ? this.__loadXMP(panorama, p => this.psv.loader.setProgress(p)) - .then(xmpPanoData => this.psv.textureLoader.loadImage(panorama).then(img => ({ img, xmpPanoData }))) - : this.psv.textureLoader.loadImage(panorama, p => this.psv.loader.setProgress(p)) - .then(img => ({ img: img, xmpPanoData: null })) - ) - .then(({ img, xmpPanoData }) => { - if (typeof newPanoData === 'function') { - newPanoData = newPanoData(img); - } - - const panoData = { - fullWidth : firstNonNull(newPanoData?.fullWidth, xmpPanoData?.fullWidth, img.width), - fullHeight : firstNonNull(newPanoData?.fullHeight, xmpPanoData?.fullHeight, img.height), - croppedWidth : firstNonNull(newPanoData?.croppedWidth, xmpPanoData?.croppedWidth, img.width), - croppedHeight: firstNonNull(newPanoData?.croppedHeight, xmpPanoData?.croppedHeight, img.height), - croppedX : firstNonNull(newPanoData?.croppedX, xmpPanoData?.croppedX, 0), - croppedY : firstNonNull(newPanoData?.croppedY, xmpPanoData?.croppedY, 0), - poseHeading : firstNonNull(newPanoData?.poseHeading, xmpPanoData?.poseHeading, 0), - posePitch : firstNonNull(newPanoData?.posePitch, xmpPanoData?.posePitch, 0), - poseRoll : firstNonNull(newPanoData?.poseRoll, xmpPanoData?.poseRoll, 0), - }; - - if (panoData.croppedWidth !== img.width || panoData.croppedHeight !== img.height) { - logWarn(`Invalid panoData, croppedWidth and/or croppedHeight is not coherent with loaded image. - panoData: ${panoData.croppedWidth}x${panoData.croppedHeight}, image: ${img.width}x${img.height}`); - } - if ((newPanoData || xmpPanoData) && panoData.fullWidth !== panoData.fullHeight * 2) { - logWarn('Invalid panoData, fullWidth should be twice fullHeight'); - } - - const texture = this.__createEquirectangularTexture(img, panoData); - - return { panorama, texture, panoData }; - }); - } - - /** - * @summary Loads the XMP data of an image - * @param {string} panorama - * @param {function(number)} [onProgress] - * @returns {Promise} - * @throws {PSV.PSVError} when the image cannot be loaded - * @private - */ - __loadXMP(panorama, onProgress) { - return this.psv.textureLoader.loadFile(panorama, onProgress) - .then(blob => this.__loadBlobAsString(blob)) - .then((binary) => { - const a = binary.indexOf(''); - const data = binary.substring(a, b); - - if (a !== -1 && b !== -1 && data.includes('GPano:')) { - return { - fullWidth : getXMPValue(data, 'FullPanoWidthPixels'), - fullHeight : getXMPValue(data, 'FullPanoHeightPixels'), - croppedWidth : getXMPValue(data, 'CroppedAreaImageWidthPixels'), - croppedHeight: getXMPValue(data, 'CroppedAreaImageHeightPixels'), - croppedX : getXMPValue(data, 'CroppedAreaLeftPixels'), - croppedY : getXMPValue(data, 'CroppedAreaTopPixels'), - poseHeading : getXMPValue(data, 'PoseHeadingDegrees'), - posePitch : getXMPValue(data, 'PosePitchDegrees'), - poseRoll : getXMPValue(data, 'PoseRollDegrees'), - }; - } - - return null; - }); - } - - /** - * @summmary read a Blob as string - * @param {Blob} blob - * @returns {Promise} - * @private - */ - __loadBlobAsString(blob) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result); - reader.onerror = reject; - reader.readAsText(blob); - }); - } - - /** - * @summary Creates the final texture from image and panorama data - * @param {Image} img - * @param {PSV.PanoData} panoData - * @returns {external:THREE.Texture} - * @private - */ - __createEquirectangularTexture(img, panoData) { - // resize image / fill cropped parts with black - if (panoData.fullWidth > SYSTEM.maxTextureWidth - || panoData.croppedWidth !== panoData.fullWidth - || panoData.croppedHeight !== panoData.fullHeight - ) { - const ratio = SYSTEM.getMaxCanvasWidth() / panoData.fullWidth; - - const resizedPanoData = { ...panoData }; - if (ratio < 1) { - resizedPanoData.fullWidth *= ratio; - resizedPanoData.fullHeight *= ratio; - resizedPanoData.croppedWidth *= ratio; - resizedPanoData.croppedHeight *= ratio; - resizedPanoData.croppedX *= ratio; - resizedPanoData.croppedY *= ratio; - } - - const buffer = document.createElement('canvas'); - buffer.width = resizedPanoData.fullWidth; - buffer.height = resizedPanoData.fullHeight; - - const ctx = buffer.getContext('2d'); - ctx.drawImage(img, - resizedPanoData.croppedX, resizedPanoData.croppedY, - resizedPanoData.croppedWidth, resizedPanoData.croppedHeight); - - return createTexture(buffer); - } - - return createTexture(img); - } - - /** - * @override - */ - createMesh(scale = 1) { - // The middle of the panorama is placed at longitude=0 - const geometry = new SphereGeometry( - SPHERE_RADIUS * scale, - this.SPHERE_SEGMENTS, - this.SPHERE_HORIZONTAL_SEGMENTS, - -Math.PI / 2 - ) - .scale(-1, 1, 1); - - const material = AbstractAdapter.createOverlayMaterial(); - - return new Mesh(geometry, material); - } - - /** - * @override - */ - setTexture(mesh, textureData) { - this.__setUniform(mesh, AbstractAdapter.OVERLAY_UNIFORMS.panorama, textureData.texture); - this.setOverlay(mesh, null); - } - - /** - * @override - */ - setOverlay(mesh, textureData, opacity) { - this.__setUniform(mesh, AbstractAdapter.OVERLAY_UNIFORMS.overlayOpacity, opacity); - if (!textureData) { - this.__setUniform(mesh, AbstractAdapter.OVERLAY_UNIFORMS.overlay, new Texture()); - } - else { - this.__setUniform(mesh, AbstractAdapter.OVERLAY_UNIFORMS.overlay, textureData.texture); - } - } - - /** - * @override - */ - setTextureOpacity(mesh, opacity) { - this.__setUniform(mesh, AbstractAdapter.OVERLAY_UNIFORMS.globalOpacity, opacity); - mesh.material.transparent = opacity < 1; - } - - /** - * @override - */ - disposeTexture(textureData) { - textureData.texture?.dispose(); - } - - /** - * @param {external:THREE.Mesh} mesh - * @param {string} uniform - * @param {*} value - * @private - */ - __setUniform(mesh, uniform, value) { - if (mesh.material.uniforms[uniform].value instanceof Texture) { - mesh.material.uniforms[uniform].value.dispose(); - } - mesh.material.uniforms[uniform].value = value; - } - -} diff --git a/src/adapters/little-planet/index.js b/src/adapters/little-planet/index.js deleted file mode 100644 index 6ece05171..000000000 --- a/src/adapters/little-planet/index.js +++ /dev/null @@ -1,164 +0,0 @@ -import { Euler, MathUtils, Matrix4, Mesh, PlaneBufferGeometry, ShaderMaterial, Texture } from 'three'; -import { CONSTANTS, DEFAULTS, EquirectangularAdapter } from '../..'; - - -DEFAULTS.defaultLat = -Math.PI / 2; - -const euler = new Euler(); - - -/** - * @summary Adapter for equirectangular panoramas displayed with little planet effect - * @memberof PSV.adapters - * @extends PSV.adapters.AbstractAdapter - */ -export class LittlePlanetAdapter extends EquirectangularAdapter { - - static id = 'little-planet'; - static supportsOverlay = false; - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv); - - this.psv.prop.littlePlanet = true; - - this.psv.on(CONSTANTS.EVENTS.SIZE_UPDATED, this); - this.psv.on(CONSTANTS.EVENTS.ZOOM_UPDATED, this); - this.psv.on(CONSTANTS.EVENTS.POSITION_UPDATED, this); - } - - /** - * @override - */ - supportsTransition() { - return false; - } - - /** - * @override - */ - supportsPreload() { - return true; - } - - /** - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case CONSTANTS.EVENTS.SIZE_UPDATED: - this.__setResolution(e.args[0]); - break; - case CONSTANTS.EVENTS.ZOOM_UPDATED: - this.__setZoom(); - break; - case CONSTANTS.EVENTS.POSITION_UPDATED: - this.__setPosition(e.args[0]); - break; - } - /* eslint-enable */ - } - - /** - * @param {PSV.Size} size - * @private - */ - __setResolution(size) { - this.uniforms.resolution.value = size.width / size.height; - } - - /** - * @private - */ - __setZoom() { - // mapping values are empirical - this.uniforms.zoom.value = Math.max(0.1, MathUtils.mapLinear(this.psv.prop.vFov, 90, 30, 50, 2)); - } - - /** - * @param {PSV.Position} position - * @private - */ - __setPosition(position) { - euler.set( - Math.PI / 2 + position.latitude, - 0, - -Math.PI / 2 - position.longitude, - 'ZYX' - ); - - this.uniforms.transform.value.makeRotationFromEuler(euler); - } - - /** - * @override - */ - createMesh() { - const geometry = new PlaneBufferGeometry(20, 10) - .translate(0, 0, -1); - - // this one was copied from https://github.com/pchen66/panolens.js - const material = new ShaderMaterial({ - uniforms: { - panorama : { value: new Texture() }, - resolution: { value: 2.0 }, - transform : { value: new Matrix4() }, - zoom : { value: 10.0 }, - opacity : { value: 1.0 }, - }, - - vertexShader: ` -varying vec2 vUv; - -void main() { - vUv = uv; - gl_Position = vec4( position, 1.0 ); -}`, - - fragmentShader: ` -uniform sampler2D panorama; -uniform float resolution; -uniform mat4 transform; -uniform float zoom; -uniform float opacity; - -varying vec2 vUv; - -const float PI = 3.1415926535897932384626433832795; - -void main() { - vec2 position = -1.0 + 2.0 * vUv; - position *= vec2( zoom * resolution, zoom * 0.5 ); - - float x2y2 = position.x * position.x + position.y * position.y; - vec3 sphere_pnt = vec3( 2. * position, x2y2 - 1. ) / ( x2y2 + 1. ); - sphere_pnt = vec3( transform * vec4( sphere_pnt, 1.0 ) ); - - vec2 sampleUV = vec2( - 1.0 - (atan(sphere_pnt.y, sphere_pnt.x) / PI + 1.0) * 0.5, - (asin(sphere_pnt.z) / PI + 0.5) - ); - - gl_FragColor = texture2D( panorama, sampleUV ); - gl_FragColor.a *= opacity; -}`, - }); - - this.uniforms = material.uniforms; - - return new Mesh(geometry, material); - } - - /** - * @override - */ - setTexture(mesh, textureData) { - mesh.material.uniforms.panorama.value.dispose(); - mesh.material.uniforms.panorama.value = textureData.texture; - } - -} diff --git a/src/adapters/shared/AbstractVideoAdapter.js b/src/adapters/shared/AbstractVideoAdapter.js deleted file mode 100644 index accf2dbd8..000000000 --- a/src/adapters/shared/AbstractVideoAdapter.js +++ /dev/null @@ -1,226 +0,0 @@ -import { VideoTexture } from 'three'; -import { AbstractAdapter, CONSTANTS, PSVError } from '../..'; - -/** - * @typedef {Object} PSV.adapters.AbstractVideoAdapter.Video - * @summary Object defining a video - * @property {string} source - */ - -/** - * @typedef {Object} PSV.adapters.AbstractVideoAdapter.Options - * @property {boolean} [autoplay=false] - automatically start the video - * @property {boolean} [muted=autoplay] - initially mute the video - */ - -/** - * @summary Base video adapters class - * @memberof PSV.adapters - * @abstract - * @private - */ -export class AbstractVideoAdapter extends AbstractAdapter { - - constructor(psv, options) { - super(psv); - - /** - * @member {PSV.adapters.AbstractVideoAdapter.Options} - * @private - */ - this.config = { - autoplay: false, - muted : options?.autoplay ?? false, - ...options, - }; - - /** - * @member {HTMLVideoElement} - * @private - */ - this.video = null; - - this.psv.on(CONSTANTS.EVENTS.BEFORE_RENDER, this); - } - - /** - * @override - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.BEFORE_RENDER, this); - - this.__removeVideo(); - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case CONSTANTS.EVENTS.BEFORE_RENDER: - if (this.video) { - this.psv.needsUpdate(); - } - break; - } - /* eslint-enable */ - } - - /** - * @override - * @param {PSV.adapters.AbstractVideoAdapter.Video} panorama - * @returns {Promise.} - */ - loadTexture(panorama) { - if (typeof panorama !== 'object' || !panorama.source) { - return Promise.reject(new PSVError('Invalid panorama configuration, are you using the right adapter?')); - } - - if (!this.psv.getPlugin('video')) { - return Promise.reject(new PSVError('Video adapters require VideoPlugin to be loaded too.')); - } - - const video = this.__createVideo(panorama.source); - - return this.__videoLoadPromise(video) - .then(() => { - const texture = new VideoTexture(video); - return { panorama, texture }; - }); - } - - /** - * @override - */ - __switchVideo(texture) { - let currentTime; - let duration; - let paused = !this.config.autoplay; - let muted = this.config.muted; - let volume = 1; - if (this.video) { - ({ currentTime, duration, paused, muted, volume } = this.video); - } - - this.__removeVideo(); - this.video = texture.image; - - // keep current time when switching resolution - if (this.video.duration === duration) { - this.video.currentTime = currentTime; - } - - // keep volume - this.video.muted = muted; - this.video.volume = volume; - - // play - if (!paused) { - this.video.play(); - } - } - - /** - * @override - */ - disposeTexture(textureData) { - if (textureData.texture) { - const video = textureData.texture.image; - video.pause(); - this.psv.container.removeChild(video); - } - textureData.texture?.dispose(); - } - - /** - * @summary Removes the current video element - * @private - */ - __removeVideo() { - if (this.video) { - this.video.pause(); - this.psv.container.removeChild(this.video); - delete this.video; - } - } - - /** - * @summary Creates a new video element - * @memberOf PSV.adapters - * @param {string} src - * @return {HTMLVideoElement} - * @private - */ - __createVideo(src) { - const video = document.createElement('video'); - video.crossOrigin = this.psv.config.withCredentials ? 'use-credentials' : 'anonymous'; - video.loop = true; - video.playsInline = true; - video.style.display = 'none'; - video.muted = this.config.muted; - video.src = src; - video.preload = 'metadata'; - - this.psv.container.appendChild(video); - - return video; - } - - /** - * @private - */ - __videoLoadPromise(video) { - const self = this; - - return new Promise((resolve, reject) => { - video.addEventListener('loadedmetadata', function onLoaded() { - if (this.video && video.duration === this.video.duration) { - resolve(self.__videoBufferPromise(video, this.video.currentTime)); - } - else { - resolve(); - } - video.removeEventListener('loadedmetadata', onLoaded); - }); - - video.addEventListener('error', function onError(err) { - reject(err); - video.removeEventListener('error', onError); - }); - }); - } - - /** - * @private - */ - __videoBufferPromise(video, currentTime) { - return new Promise((resolve) => { - function onBuffer() { - const buffer = video.buffered; - for (let i = 0, l = buffer.length; i < l; i++) { - if (buffer.start(i) <= video.currentTime && buffer.end(i) >= video.currentTime) { - video.pause(); - video.removeEventListener('buffer', onBuffer); - video.removeEventListener('progress', onBuffer); - resolve(); - break; - } - } - } - - // try to reduce the switching time by preloading in advance - // FIXME find a better way ? - video.currentTime = Math.min(currentTime + 2000, video.duration.currentTime); - video.muted = true; - - video.addEventListener('buffer', onBuffer); - video.addEventListener('progress', onBuffer); - - video.play(); - }); - } - -} diff --git a/src/adapters/shared/Queue.js b/src/adapters/shared/Queue.js deleted file mode 100644 index b48298c48..000000000 --- a/src/adapters/shared/Queue.js +++ /dev/null @@ -1,71 +0,0 @@ -import { Task } from './Task'; - -/** - * @summary Loading queue - * @memberOf PSV.adapters - * @private - */ -export class Queue { - - /** - * @param {int} concurency - */ - constructor(concurency = 4) { - this.concurency = concurency; - this.runningTasks = {}; - this.tasks = {}; - } - - enqueue(task) { - this.tasks[task.id] = task; - } - - clear() { - Object.values(this.tasks).forEach(task => task.cancel()); - this.tasks = {}; - this.runningTasks = {}; - } - - setPriority(taskId, priority) { - const task = this.tasks[taskId]; - if (task) { - task.priority = priority; - if (task.status === Task.STATUS.DISABLED) { - task.status = Task.STATUS.PENDING; - } - } - } - - disableAllTasks() { - Object.values(this.tasks).forEach((task) => { - task.status = Task.STATUS.DISABLED; - }); - } - - start() { - if (Object.keys(this.runningTasks).length >= this.concurency) { - return; - } - - const nextTask = Object.values(this.tasks) - .filter(task => task.status === Task.STATUS.PENDING) - .sort((a, b) => b.priority - a.priority) - .pop(); - - if (nextTask) { - this.runningTasks[nextTask.id] = true; - - nextTask.start() - .then(() => { - if (!nextTask.isCancelled()) { - delete this.tasks[nextTask.id]; - delete this.runningTasks[nextTask.id]; - this.start(); - } - }); - - this.start(); // start tasks until max concurrency is reached - } - } - -} diff --git a/src/adapters/shared/Task.js b/src/adapters/shared/Task.js deleted file mode 100644 index 7e4aa9437..000000000 --- a/src/adapters/shared/Task.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @summary Loading task - * @memberOf PSV.adapters - * @private - */ -export class Task { - - static STATUS = { - DISABLED : -1, - PENDING : 0, - RUNNING : 1, - CANCELLED: 2, - DONE : 3, - ERROR : 4, - }; - - /** - * @param {string} id - * @param {number} priority - * @param {function(Task): Promise} fn - */ - constructor(id, priority, fn) { - this.id = id; - this.priority = priority; - this.fn = fn; - this.status = Task.STATUS.PENDING; - } - - start() { - this.status = Task.STATUS.RUNNING; - return this.fn(this) - .then(() => { - this.status = Task.STATUS.DONE; - }, () => { - this.status = Task.STATUS.ERROR; - }); - } - - cancel() { - this.status = Task.STATUS.CANCELLED; - } - - isCancelled() { - return this.status === Task.STATUS.CANCELLED; - } - -} diff --git a/src/adapters/shared/tiles-utils.js b/src/adapters/shared/tiles-utils.js deleted file mode 100644 index 287b12df8..000000000 --- a/src/adapters/shared/tiles-utils.js +++ /dev/null @@ -1,86 +0,0 @@ -import { CanvasTexture, LineSegments, Mesh, MeshBasicMaterial, SphereGeometry, WireframeGeometry } from 'three'; -import { SYSTEM, utils } from '../..'; - -/** - * @summary Generates an material for errored tiles - * @memberOf PSV.adapters - * @return {external:THREE.MeshBasicMaterial} - * @private - */ -export function buildErrorMaterial(width, height) { - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - - const ctx = canvas.getContext('2d'); - - ctx.fillStyle = '#333'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.font = `${canvas.width / 5}px serif`; - ctx.fillStyle = '#a22'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText('⚠', canvas.width / 2, canvas.height / 2); - - const texture = new CanvasTexture(canvas); - return new MeshBasicMaterial({ map: texture }); -} - -/** - * @summary Create the texture for the base image - * @memberOf PSV.adapters - * @param {HTMLImageElement} img - * @param {boolean} blur - * @param {function} getHeight - * @return {external:THREE.Texture} - * @private - */ -export function createBaseTexture(img, blur, getHeight) { - if (blur || img.width > SYSTEM.maxTextureWidth) { - const ratio = Math.min(1, SYSTEM.getMaxCanvasWidth() / img.width); - - const buffer = document.createElement('canvas'); - buffer.width = img.width * ratio; - buffer.height = getHeight(img.width); - - const ctx = buffer.getContext('2d'); - if (blur) { - ctx.filter = 'blur(1px)'; - } - ctx.drawImage(img, 0, 0, buffer.width, buffer.height); - - return utils.createTexture(buffer); - } - - return utils.createTexture(img); -} - -/** - * @summary Creates a wireframe geometry, for debug - * @memberOf PSV.adapters - * @param {THREE.BufferGeometry} geometry - * @return {THREE.Object3D} - * @private - */ -export function createWireFrame(geometry) { - const wireframe = new WireframeGeometry(geometry); - const line = new LineSegments(wireframe); - line.material.depthTest = false; - line.material.opacity = 0.25; - line.material.transparent = true; - return line; -} - -/** - * @summary Creates a small red sphere, for debug - * @memberOf PSV.adapters - * @return {THREE.Object3D} - * @private - */ -export function createDot(x, y, z) { - const geom = new SphereGeometry(0.1); - const material = new MeshBasicMaterial({ color: 0xff0000 }); - const mesh = new Mesh(geom, material); - mesh.position.set(x, y, z); - return mesh; -} diff --git a/src/buttons/AbstractButton.js b/src/buttons/AbstractButton.js deleted file mode 100644 index ade0d2324..000000000 --- a/src/buttons/AbstractButton.js +++ /dev/null @@ -1,242 +0,0 @@ -import { AbstractComponent } from '../components/AbstractComponent'; -import { KEY_CODES } from '../data/constants'; -import { PSVError } from '../PSVError'; -import { isPlainObject, toggleClass } from '../utils'; - -/** - * @namespace PSV.buttons - */ - -/** - * @summary Base navbar button class - * @extends PSV.components.AbstractComponent - * @memberof PSV.buttons - * @abstract - */ -export class AbstractButton extends AbstractComponent { - - /** - * @summary Unique identifier of the button - * @member {string} - * @readonly - * @static - */ - static id = null; - - /** - * @summary Identifier to declare a group of buttons - * @member {string} - * @readonly - * @static - */ - static groupId = null; - - /** - * @summary SVG icon name injected in the button - * @member {string} - * @readonly - * @static - */ - static icon = null; - - /** - * @summary SVG icon name injected in the button when it is active - * @member {string} - * @readonly - * @static - */ - static iconActive = null; - - /** - * @param {PSV.components.Navbar} navbar - * @param {string} [className] - Additional CSS classes - * @param {boolean} [collapsable=false] - `true` if the button can be moved to menu when the navbar is too small - * @param {boolean} [tabbable=true] - `true` if the button is accessible with the keyboard - */ - constructor(navbar, className = '', collapsable = false, tabbable = true) { - super(navbar, 'psv-button ' + className); - - /** - * @override - * @property {string} id - Unique identifier of the button - * @property {boolean} enabled - * @property {boolean} supported - * @property {boolean} collapsed - * @property {boolean} active - * @property {number} width - */ - this.prop = { - ...this.prop, - id : this.constructor.id, - collapsable: collapsable, - enabled : true, - supported : true, - collapsed : false, - active : false, - width : this.container.offsetWidth, - }; - - if (this.constructor.icon) { - this.__setIcon(this.constructor.icon); - } - - if (this.prop.id && this.psv.config.lang[this.prop.id]) { - this.container.title = this.psv.config.lang[this.prop.id]; - } - - if (tabbable) { - this.container.tabIndex = 0; - } - - this.container.addEventListener('click', (e) => { - if (this.prop.enabled) { - this.onClick(); - } - e.stopPropagation(); - }); - - this.container.addEventListener('keydown', (e) => { - if (e.key === KEY_CODES.Enter && this.prop.enabled) { - this.onClick(); - e.stopPropagation(); - } - }); - } - - /** - * @package - */ - checkSupported() { - const supportedOrObject = this.isSupported(); - if (isPlainObject(supportedOrObject)) { - if (supportedOrObject.initial === false) { - this.hide(); - this.prop.supported = false; - } - - supportedOrObject.promise.then((supported) => { - if (!this.prop) { - return; // the component has been destroyed - } - this.prop.supported = supported; - this.toggle(supported); - }); - } - else { - this.prop.supported = supportedOrObject; - if (!supportedOrObject) { - this.hide(); - } - } - } - - /** - * @summary Checks if the button can be displayed - * @returns {boolean|{initial: boolean, promise: Promise}} - */ - isSupported() { - return true; - } - - /** - * @summary Changes the active state of the button - * @param {boolean} [active] - forced state - */ - toggleActive(active) { - this.prop.active = active !== undefined ? active : !this.prop.active; - toggleClass(this.container, 'psv-button--active', this.prop.active); - - if (this.constructor.iconActive) { - this.__setIcon(this.prop.active ? this.constructor.iconActive : this.constructor.icon); - } - } - - /** - * @override - */ - show(refresh = true) { - if (!this.isVisible()) { - this.prop.visible = true; - if (!this.prop.collapsed) { - this.container.style.display = ''; - } - if (refresh) { - this.psv.refreshUi(`show button ${this.prop.id}`); - } - } - } - - /** - * @override - */ - hide(refresh = true) { - if (this.isVisible()) { - this.prop.visible = false; - this.container.style.display = 'none'; - if (refresh) { - this.psv.refreshUi(`hide button ${this.prop.id}`); - } - } - } - - /** - * @summary Disables the button - */ - disable() { - this.container.classList.add('psv-button--disabled'); - this.prop.enabled = false; - } - - /** - * @summary Enables the button - */ - enable() { - this.container.classList.remove('psv-button--disabled'); - this.prop.enabled = true; - } - - /** - * @summary Collapses the button in the navbar menu - */ - collapse() { - this.prop.collapsed = true; - this.container.style.display = 'none'; - } - - /** - * @summary Uncollapses the button from the navbar menu - */ - uncollapse() { - this.prop.collapsed = false; - if (this.prop.visible) { - this.container.style.display = ''; - } - } - - /** - * @summary Set the button icon - * @param {string} icon SVG - * @param {HTMLElement} [container] - default is the main button container - * @private - */ - __setIcon(icon, container = this.container) { - if (icon) { - container.innerHTML = icon; - // className is read-only on SVGElement - container.querySelector('svg').classList.add('psv-button-svg'); - } - else { - container.innerHTML = ''; - } - } - - /** - * @summary Action when the button is clicked - * @private - * @abstract - */ - onClick() { - throw new PSVError(`onClick not implemented for button "${this.prop.id}".`); - } - -} diff --git a/src/buttons/AbstractMoveButton.js b/src/buttons/AbstractMoveButton.js deleted file mode 100644 index bd8b85f93..000000000 --- a/src/buttons/AbstractMoveButton.js +++ /dev/null @@ -1,132 +0,0 @@ -import { KEY_CODES } from '../data/constants'; -import { SYSTEM } from '../data/system'; -import arrow from '../icons/arrow.svg'; -import { PressHandler } from '../utils/PressHandler'; -import { AbstractButton } from './AbstractButton'; - -export function getOrientedArrow(direction) { - let angle = 0; - switch (direction) { - // @formatter:off - case 'up': angle = 90; break; - case 'right': angle = 180; break; - case 'down': angle = -90; break; - default: angle = 0; break; - // @formatter:on - } - - return arrow.replace('rotate(0', `rotate(${angle}`); -} - -/** - * @summary Navigation bar move button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class AbstractMoveButton extends AbstractButton { - - static groupId = 'move'; - - /** - * @param {PSV.components.Navbar} navbar - * @param {number} value - */ - constructor(navbar, value) { - super(navbar, 'psv-button--hover-scale psv-move-button'); - - this.container.title = this.psv.config.lang.move; - - /** - * @override - * @property {{longitude: boolean, latitude: boolean}} value - * @property {PressHandler} handler - */ - this.prop = { - ...this.prop, - value : value, - handler: new PressHandler(), - }; - - this.container.addEventListener('mousedown', this); - this.container.addEventListener('keydown', this); - this.container.addEventListener('keyup', this); - this.psv.container.addEventListener('mouseup', this); - this.psv.container.addEventListener('touchend', this); - } - - /** - * @override - */ - destroy() { - this.__onMouseUp(); - - this.psv.container.removeEventListener('mouseup', this); - this.psv.container.removeEventListener('touchend', this); - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case 'mousedown': this.__onMouseDown(); break; - case 'mouseup': this.__onMouseUp(); break; - case 'touchend': this.__onMouseUp(); break; - case 'keydown': e.key === KEY_CODES.Enter && this.__onMouseDown(); break; - case 'keyup': e.key === KEY_CODES.Enter && this.__onMouseUp(); break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @override - */ - isSupported() { - return { - initial: !SYSTEM.isTouchEnabled.initial, - promise: SYSTEM.isTouchEnabled.promise.then(enabled => !enabled), - }; - } - - /** - * @override - */ - onClick() { - // nothing - } - - /** - * @private - */ - __onMouseDown() { - if (!this.prop.enabled) { - return; - } - - this.psv.__stopAll(); - this.psv.dynamics.position.roll(this.prop.value); - this.prop.handler.down(); - } - - /** - * @private - */ - __onMouseUp() { - if (!this.prop.enabled) { - return; - } - - this.prop.handler.up(() => { - this.psv.dynamics.position.stop(); - this.psv.resetIdleTimer(); - }); - } - -} diff --git a/src/buttons/AbstractZoomButton.js b/src/buttons/AbstractZoomButton.js deleted file mode 100644 index b611bda83..000000000 --- a/src/buttons/AbstractZoomButton.js +++ /dev/null @@ -1,111 +0,0 @@ -import { KEY_CODES } from '../data/constants'; -import { SYSTEM } from '../data/system'; -import { PressHandler } from '../utils/PressHandler'; -import { AbstractButton } from './AbstractButton'; - -/** - * @summary Navigation bar zoom button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class AbstractZoomButton extends AbstractButton { - - static groupId = 'zoom'; - - /** - * @param {PSV.components.Navbar} navbar - * @param {number} value - */ - constructor(navbar, value) { - super(navbar, 'psv-button--hover-scale psv-zoom-button'); - - /** - * @override - * @property {boolean} value - * @property {PressHandler} handler - */ - this.prop = { - ...this.prop, - value : value, - handler: new PressHandler(), - }; - - this.container.addEventListener('mousedown', this); - this.container.addEventListener('keydown', this); - this.container.addEventListener('keyup', this); - this.psv.container.addEventListener('mouseup', this); - this.psv.container.addEventListener('touchend', this); - } - - /** - * @override - */ - destroy() { - this.__onMouseUp(); - - this.psv.container.removeEventListener('mouseup', this); - this.psv.container.removeEventListener('touchend', this); - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case 'mousedown': this.__onMouseDown(); break; - case 'mouseup': this.__onMouseUp(); break; - case 'touchend': this.__onMouseUp(); break; - case 'keydown': e.key === KEY_CODES.Enter && this.__onMouseDown(); break; - case 'keyup': e.key === KEY_CODES.Enter && this.__onMouseUp(); break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @override - */ - isSupported() { - return { - initial: !SYSTEM.isTouchEnabled.initial, - promise: SYSTEM.isTouchEnabled.promise.then(enabled => !enabled), - }; - } - - /** - * @override - */ - onClick() { - // nothing - } - - /** - * @private - */ - __onMouseDown() { - if (!this.prop.enabled) { - return; - } - - this.psv.dynamics.zoom.roll(this.prop.value); - this.prop.handler.down(); - } - - /** - * @private - */ - __onMouseUp() { - if (!this.prop.enabled) { - return; - } - - this.prop.handler.up(() => this.psv.dynamics.zoom.stop()); - } - -} diff --git a/src/buttons/AutorotateButton.js b/src/buttons/AutorotateButton.js deleted file mode 100644 index 5ac7aa6f8..000000000 --- a/src/buttons/AutorotateButton.js +++ /dev/null @@ -1,62 +0,0 @@ -import { EVENTS } from '../data/constants'; -import playActive from '../icons/play-active.svg'; -import play from '../icons/play.svg'; -import { AbstractButton } from './AbstractButton'; - -/** - * @summary Navigation bar autorotate button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class AutorotateButton extends AbstractButton { - - static id = 'autorotate'; - static icon = play; - static iconActive = playActive; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-autorotate-button', true); - - this.psv.on(EVENTS.AUTOROTATE, this); - } - - /** - * @override - */ - destroy() { - this.psv.off(EVENTS.AUTOROTATE, this); - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case EVENTS.AUTOROTATE: this.toggleActive(e.args[0]); break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @override - * @description Toggles autorotate - */ - onClick() { - if (this.psv.isAutorotateEnabled()) { - this.psv.config.autorotateIdle = false; - this.psv.resetIdleTimer(); - } - this.psv.toggleAutorotate(); - } - -} diff --git a/src/buttons/CustomButton.js b/src/buttons/CustomButton.js deleted file mode 100644 index 9e8c947e9..000000000 --- a/src/buttons/CustomButton.js +++ /dev/null @@ -1,74 +0,0 @@ -import { addClasses } from '../utils'; -import { AbstractButton } from './AbstractButton'; - -/** - * @summary Navigation bar custom button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class CustomButton extends AbstractButton { - - /** - * @param {PSV.components.Navbar} navbar - * @param {PSV.NavbarCustomButton} config - */ - constructor(navbar, config) { - super(navbar, 'psv-custom-button', config.collapsable !== false, config.tabbable !== false); - - /** - * @member {Object} - * @readonly - * @private - */ - this.config = config; - - if (this.config.id) { - this.prop.id = this.config.id; - } - else { - this.prop.id = 'psvButton-' + Math.random().toString(36).substr(2, 9); - } - - if (this.config.className) { - addClasses(this.container, this.config.className); - } - - if (this.config.title) { - this.container.title = this.config.title; - } - - if (this.config.content) { - this.container.innerHTML = this.config.content; - } - - this.prop.width = this.container.offsetWidth; - - if (this.config.enabled === false) { - this.disable(); - } - - if (this.config.visible === false) { - this.hide(); - } - } - - /** - * @override - */ - destroy() { - delete this.config; - - super.destroy(); - } - - /** - * @override - * @description Calls user method - */ - onClick() { - if (this.config.onClick) { - this.config.onClick.call(this.psv, this.psv); - } - } - -} diff --git a/src/buttons/DescriptionButton.js b/src/buttons/DescriptionButton.js deleted file mode 100644 index a75499b38..000000000 --- a/src/buttons/DescriptionButton.js +++ /dev/null @@ -1,168 +0,0 @@ -import { EVENTS, IDS } from '../data/constants'; -import info from '../icons/info.svg'; -import { AbstractButton } from './AbstractButton'; - -const MODE_NOTIF = 1; -const MODE_PANEL = 2; - -/** - * @summary Navigation bar description button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class DescriptionButton extends AbstractButton { - - static id = 'description'; - static icon = info; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-description-button'); - - /** - * @override - * @property {string} mode - notification or panel - */ - this.prop = { - ...this.prop, - mode: null, - }; - - this.psv.on(EVENTS.HIDE_NOTIFICATION, this); - this.psv.on(EVENTS.SHOW_NOTIFICATION, this); - this.psv.on(EVENTS.CLOSE_PANEL, this); - this.psv.on(EVENTS.OPEN_PANEL, this); - } - - /** - * @override - */ - destroy() { - this.psv.off(EVENTS.HIDE_NOTIFICATION, this); - this.psv.off(EVENTS.SHOW_NOTIFICATION, this); - this.psv.off(EVENTS.CLOSE_PANEL, this); - this.psv.off(EVENTS.OPEN_PANEL, this); - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - if (!this.prop.mode) { - return; - } - - let closed = false; - switch (e.type) { - case EVENTS.HIDE_NOTIFICATION: - closed = this.prop.mode === MODE_NOTIF; - break; - - case EVENTS.SHOW_NOTIFICATION: - closed = this.prop.mode === MODE_NOTIF && e.args[0] !== IDS.DESCRIPTION; - break; - - case EVENTS.CLOSE_PANEL: - closed = this.prop.mode === MODE_PANEL; - break; - - case EVENTS.OPEN_PANEL: - closed = this.prop.mode === MODE_PANEL && e.args[0] !== IDS.DESCRIPTION; - break; - - default: - } - - if (closed) { - this.toggleActive(false); - this.prop.mode = null; - } - } - - /** - * @override - */ - hide(refresh) { - super.hide(refresh); - - if (this.prop.mode) { - this.__close(); - } - } - - /** - * This button can only be refresh from NavbarCaption - * @override - */ - refreshUi(refresh = false) { - if (refresh) { - const caption = this.psv.navbar.getButton('caption', false); - const captionHidden = caption && !caption.isVisible(); - const hasDescription = !!this.psv.config.description; - - if (captionHidden || hasDescription) { - this.show(false); - } - else { - this.hide(false); - } - } - } - - /** - * @override - * @description Toggles caption - */ - onClick() { - if (this.prop.mode) { - this.__close(); - } - else { - this.__open(); - } - } - - /** - * @private - */ - __close() { - switch (this.prop.mode) { - case MODE_NOTIF: - this.psv.notification.hide(IDS.DESCRIPTION); - break; - case MODE_PANEL: - this.psv.panel.hide(IDS.DESCRIPTION); - break; - default: - } - } - - /** - * @private - */ - __open() { - this.toggleActive(true); - - if (this.psv.config.description) { - this.prop.mode = MODE_PANEL; - this.psv.panel.show({ - id : IDS.DESCRIPTION, - content: `${this.psv.config.caption ? `

${this.psv.config.caption}

` : ''}${this.psv.config.description}`, - }); - } - else { - this.prop.mode = MODE_NOTIF; - this.psv.notification.show({ - id : IDS.DESCRIPTION, - content: this.psv.config.caption, - }); - } - } - -} diff --git a/src/buttons/DownloadButton.js b/src/buttons/DownloadButton.js deleted file mode 100644 index f958d6051..000000000 --- a/src/buttons/DownloadButton.js +++ /dev/null @@ -1,50 +0,0 @@ -import download from '../icons/download.svg'; -import { AbstractButton } from './AbstractButton'; - -/** - * @summary Navigation bar download button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class DownloadButton extends AbstractButton { - - static id = 'download'; - static icon = download; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-download-button', true); - } - - /** - * @override - * @description Asks the browser to download the panorama source file - */ - onClick() { - const link = document.createElement('a'); - link.href = this.psv.config.downloadUrl || this.psv.config.panorama; - link.download = link.href.split('/').pop(); - this.psv.container.appendChild(link); - link.click(); - - setTimeout(() => { - this.psv.container.removeChild(link); - }, 100); - } - - /** - * @override - */ - refreshUi() { - const supported = this.psv.adapter.constructor.supportsDownload || this.psv.config.downloadUrl; - if (supported && !this.prop.visible) { - this.show(); - } - else if (!supported && this.prop.visible) { - this.hide(); - } - } - -} diff --git a/src/buttons/FullscreenButton.js b/src/buttons/FullscreenButton.js deleted file mode 100644 index 79ec8c14d..000000000 --- a/src/buttons/FullscreenButton.js +++ /dev/null @@ -1,58 +0,0 @@ -import { EVENTS } from '../data/constants'; -import fullscreenIn from '../icons/fullscreen-in.svg'; -import fullscreenOut from '../icons/fullscreen-out.svg'; -import { AbstractButton } from './AbstractButton'; - -/** - * @summary Navigation bar fullscreen button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class FullscreenButton extends AbstractButton { - - static id = 'fullscreen'; - static icon = fullscreenIn; - static iconActive = fullscreenOut; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-fullscreen-button'); - - this.psv.on(EVENTS.FULLSCREEN_UPDATED, this); - } - - /** - * @override - */ - destroy() { - this.psv.off(EVENTS.FULLSCREEN_UPDATED, this); - - super.destroy(); - } - - /** - * Handle events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case EVENTS.FULLSCREEN_UPDATED: this.toggleActive(e.args[0]); break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @override - * @description Toggles fullscreen - */ - onClick() { - this.psv.toggleFullscreen(); - } - -} diff --git a/src/buttons/MenuButton.js b/src/buttons/MenuButton.js deleted file mode 100644 index f0f556f4e..000000000 --- a/src/buttons/MenuButton.js +++ /dev/null @@ -1,135 +0,0 @@ -import { EVENTS, IDS } from '../data/constants'; -import menuIcon from '../icons/menu.svg'; -import { dasherize, getClosest } from '../utils'; -import { AbstractButton } from './AbstractButton'; - -/** - * @summary Navigation bar menu button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class MenuButton extends AbstractButton { - - static id = 'menu'; - static icon = menuIcon; - - /** - * @summary Property name added to buttons list - * @type {string} - * @constant - */ - static BUTTON_DATA = 'psvButton'; - - /** - * @summary Menu template - * @param {AbstractButton[]} buttons - * @param {PSV.Viewer} psv - * @param {string} dataKey - * @returns {string} - */ - static MENU_TEMPLATE = (buttons, psv, dataKey) => ` -
-

${menuIcon} ${psv.config.lang.menu}

-
    - ${buttons.map(button => ` -
  • - ${button.container.innerHTML} - ${button.container.title} -
  • - `).join('')} -
-
-`; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-menu-button'); - - this.psv.on(EVENTS.OPEN_PANEL, this); - this.psv.on(EVENTS.CLOSE_PANEL, this); - - super.hide(); - } - - /** - * @override - */ - destroy() { - this.psv.off(EVENTS.OPEN_PANEL, this); - this.psv.off(EVENTS.CLOSE_PANEL, this); - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case EVENTS.OPEN_PANEL: this.toggleActive(e.args[0] === IDS.MENU); break; - case EVENTS.CLOSE_PANEL: this.toggleActive(false); break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @override - */ - hide(refresh) { - super.hide(refresh); - this.__hideMenu(); - } - - /** - * @override - */ - show(refresh) { - super.show(refresh); - - if (this.prop.active) { - this.__showMenu(); - } - } - - /** - * @override - * @description Toggles menu - */ - onClick() { - if (this.prop.active) { - this.__hideMenu(); - } - else { - this.__showMenu(); - } - } - - __showMenu() { - this.psv.panel.show({ - id : IDS.MENU, - content : MenuButton.MENU_TEMPLATE(this.parent.collapsed, this.psv, dasherize(MenuButton.BUTTON_DATA)), - noMargin : true, - clickHandler: (e) => { - const li = e.target ? getClosest(e.target, 'li') : undefined; - const buttonId = li ? li.dataset[MenuButton.BUTTON_DATA] : undefined; - - if (buttonId) { - this.parent.getButton(buttonId).onClick(); - this.__hideMenu(); - } - }, - }); - } - - __hideMenu() { - this.psv.panel.hide(IDS.MENU); - } - -} diff --git a/src/buttons/MoveDownButton.js b/src/buttons/MoveDownButton.js deleted file mode 100644 index ac79b6fd2..000000000 --- a/src/buttons/MoveDownButton.js +++ /dev/null @@ -1,20 +0,0 @@ -import { AbstractMoveButton, getOrientedArrow } from './AbstractMoveButton'; - -/** - * @summary Navigation bar move down button class - * @extends PSV.buttons.AbstractMoveButton - * @memberof PSV.buttons - */ -export class MoveDownButton extends AbstractMoveButton { - - static id = 'moveDown'; - static icon = getOrientedArrow('down'); - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, { latitude: true }); - } - -} diff --git a/src/buttons/MoveLeftButton.js b/src/buttons/MoveLeftButton.js deleted file mode 100644 index e8352bef2..000000000 --- a/src/buttons/MoveLeftButton.js +++ /dev/null @@ -1,20 +0,0 @@ -import { AbstractMoveButton, getOrientedArrow } from './AbstractMoveButton'; - -/** - * @summary Navigation bar move left button class - * @extends PSV.buttons.AbstractMoveButton - * @memberof PSV.buttons - */ -export class MoveLeftButton extends AbstractMoveButton { - - static id = 'moveLeft'; - static icon = getOrientedArrow('left'); - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, { longitude: true }); - } - -} diff --git a/src/buttons/MoveRightButton.js b/src/buttons/MoveRightButton.js deleted file mode 100644 index 34743d461..000000000 --- a/src/buttons/MoveRightButton.js +++ /dev/null @@ -1,20 +0,0 @@ -import { AbstractMoveButton, getOrientedArrow } from './AbstractMoveButton'; - -/** - * @summary Navigation bar move right button class - * @extends PSV.buttons.AbstractMoveButton - * @memberof PSV.buttons - */ -export class MoveRightButton extends AbstractMoveButton { - - static id = 'moveRight'; - static icon = getOrientedArrow('right'); - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, { longitude: false }); - } - -} diff --git a/src/buttons/MoveUpButton.js b/src/buttons/MoveUpButton.js deleted file mode 100644 index bdbf5c347..000000000 --- a/src/buttons/MoveUpButton.js +++ /dev/null @@ -1,20 +0,0 @@ -import { AbstractMoveButton, getOrientedArrow } from './AbstractMoveButton'; - -/** - * @summary Navigation bar move up button class - * @extends PSV.buttons.AbstractMoveButton - * @memberof PSV.buttons - */ -export class MoveUpButton extends AbstractMoveButton { - - static id = 'moveUp'; - static icon = getOrientedArrow('up'); - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, { latitude: false }); - } - -} diff --git a/src/buttons/ZoomInButton.js b/src/buttons/ZoomInButton.js deleted file mode 100644 index ca5a1bda9..000000000 --- a/src/buttons/ZoomInButton.js +++ /dev/null @@ -1,21 +0,0 @@ -import { AbstractZoomButton } from './AbstractZoomButton'; -import zoomIn from '../icons/zoom-in.svg'; - -/** - * @summary Navigation bar zoom-in button class - * @extends PSV.buttons.AbstractZoomButton - * @memberof PSV.buttons - */ -export class ZoomInButton extends AbstractZoomButton { - - static id = 'zoomIn'; - static icon = zoomIn; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, false); - } - -} diff --git a/src/buttons/ZoomOutButton.js b/src/buttons/ZoomOutButton.js deleted file mode 100644 index 3d4393dfd..000000000 --- a/src/buttons/ZoomOutButton.js +++ /dev/null @@ -1,21 +0,0 @@ -import { AbstractZoomButton } from './AbstractZoomButton'; -import zoomOut from '../icons/zoom-out.svg'; - -/** - * @summary Navigation bar zoom-out button class - * @extends PSV.buttons.AbstractZoomButton - * @memberof PSV.buttons - */ -export class ZoomOutButton extends AbstractZoomButton { - - static id = 'zoomOut'; - static icon = zoomOut; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, true); - } - -} diff --git a/src/buttons/ZoomRangeButton.js b/src/buttons/ZoomRangeButton.js deleted file mode 100644 index 43982cfb1..000000000 --- a/src/buttons/ZoomRangeButton.js +++ /dev/null @@ -1,155 +0,0 @@ -import { EVENTS } from '../data/constants'; -import { SYSTEM } from '../data/system'; -import { getStyle, Slider } from '../utils'; -import { AbstractButton } from './AbstractButton'; - -/** - * @summary Navigation bar zoom button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class ZoomRangeButton extends AbstractButton { - - static id = 'zoomRange'; - static groupId = 'zoom'; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-zoom-range', false, false); - - /** - * @override - * @property {number} mediaMinWidth - */ - this.prop = { - ...this.prop, - mediaMinWidth: 0, - }; - - /** - * @member {HTMLElement} - * @readonly - * @private - */ - this.zoomRange = document.createElement('div'); - this.zoomRange.className = 'psv-zoom-range-line'; - this.container.appendChild(this.zoomRange); - - /** - * @member {HTMLElement} - * @readonly - * @private - */ - this.zoomValue = document.createElement('div'); - this.zoomValue.className = 'psv-zoom-range-handle'; - this.zoomRange.appendChild(this.zoomValue); - - /** - * @member {PSV.Slider} - * @readonly - * @private - */ - this.slider = new Slider({ - container: this.container, - direction: Slider.HORIZONTAL, - onUpdate : e => this.__onSliderUpdate(e), - }); - - this.prop.mediaMinWidth = parseInt(getStyle(this.container, 'maxWidth'), 10); - - this.psv.on(EVENTS.ZOOM_UPDATED, this); - if (this.psv.prop.ready) { - this.__moveZoomValue(this.psv.getZoomLevel()); - } - else { - this.psv.once(EVENTS.READY, this); - } - - this.refreshUi(); - } - - /** - * @override - */ - destroy() { - this.slider.destroy(); - - delete this.zoomRange; - delete this.zoomValue; - - this.psv.off(EVENTS.ZOOM_UPDATED, this); - this.psv.off(EVENTS.READY, this); - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case EVENTS.ZOOM_UPDATED: this.__moveZoomValue(e.args[0]); break; - case EVENTS.READY: this.__moveZoomValue(this.psv.getZoomLevel()); break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @override - */ - isSupported() { - return { - initial: !SYSTEM.isTouchEnabled.initial, - promise: SYSTEM.isTouchEnabled.promise.then(enabled => !enabled), - }; - } - - /** - * @override - */ - refreshUi() { - if (this.prop.supported) { - if (this.psv.prop.size.width <= this.prop.mediaMinWidth && this.prop.visible) { - this.hide(); - } - else if (this.psv.prop.size.width > this.prop.mediaMinWidth && !this.prop.visible) { - this.show(); - } - } - } - - /** - * @override - */ - onClick() { - // nothing - } - - /** - * @summary Moves the zoom cursor - * @param {number} level - * @private - */ - __moveZoomValue(level) { - this.zoomValue.style.left = (level / 100 * this.zoomRange.offsetWidth - this.zoomValue.offsetWidth / 2) + 'px'; - } - - - /** - * @summary Zoom change - * @private - */ - __onSliderUpdate(e) { - if (e.mousedown) { - this.psv.zoom(e.value * 100); - } - } - -} diff --git a/src/components/AbstractComponent.js b/src/components/AbstractComponent.js deleted file mode 100644 index b01605558..000000000 --- a/src/components/AbstractComponent.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @namespace PSV.components - */ - -/** - * @summary Base component class - * @memberof PSV.components - * @abstract - */ -export class AbstractComponent { - - /** - * @param {PSV.Viewer | PSV.components.AbstractComponent} parent - * @param {string} className - CSS class added to the component's container - */ - constructor(parent, className) { - /** - * @summary Reference to main controller - * @type {PSV.Viewer} - * @readonly - */ - this.psv = parent.psv || parent; - - /** - * @member {PSV.Viewer|PSV.components.AbstractComponent} - * @readonly - */ - this.parent = parent; - this.parent.children.push(this); - - /** - * @summary All child components - * @type {PSV.components.AbstractComponent[]} - * @readonly - * @package - */ - this.children = []; - - /** - * @summary Internal properties - * @member {Object} - * @protected - * @property {boolean} visible - Visibility of the component - */ - this.prop = { - visible: true, - }; - - /** - * @member {HTMLElement} - * @readonly - */ - this.container = document.createElement('div'); - this.container.className = className; - this.parent.container.appendChild(this.container); - } - - /** - * @summary Destroys the component - * @protected - */ - destroy() { - this.parent.container.removeChild(this.container); - - const childIdx = this.parent.children.indexOf(this); - if (childIdx !== -1) { - this.parent.children.splice(childIdx, 1); - } - - this.children.slice().forEach(child => child.destroy()); - this.children.length = 0; - - delete this.container; - delete this.parent; - delete this.psv; - delete this.prop; - } - - /** - * @summary Refresh UI - * @description Must be be a very lightweight operation - * @package - */ - refreshUi() { - this.children.every((child) => { - child.refreshUi(); - return this.psv.prop.uiRefresh === true; - }); - } - - /** - * @summary Displays or hides the component - * @param {boolean} [visible] - forced state - */ - toggle(visible) { - if (visible === false || visible === undefined && this.isVisible()) { - this.hide(); - } - else if (visible === true || visible === undefined && !this.isVisible()) { - this.show(); - } - } - - /** - * @summary Hides the component - */ - hide() { - this.container.style.display = 'none'; - this.prop.visible = false; - } - - /** - * @summary Displays the component - */ - show() { - this.container.style.display = ''; - this.prop.visible = true; - } - - /** - * @summary Checks if the component is visible - * @returns {boolean} - */ - isVisible() { - return this.prop.visible; - } - -} diff --git a/src/components/Loader.js b/src/components/Loader.js deleted file mode 100644 index dd88f770d..000000000 --- a/src/components/Loader.js +++ /dev/null @@ -1,122 +0,0 @@ -import { MathUtils } from 'three'; -import { EVENTS } from '../data/constants'; -import { SYSTEM } from '../data/system'; -import { getStyle } from '../utils'; -import { AbstractComponent } from './AbstractComponent'; - -/** - * @summary Loader component - * @extends PSV.components.AbstractComponent - * @memberof PSV.components - */ -export class Loader extends AbstractComponent { - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv, 'psv-loader-container'); - - /** - * @summary Inner container for vertical center - * @member {HTMLElement} - * @readonly - * @private - */ - this.loader = document.createElement('div'); - this.loader.className = 'psv-loader'; - this.container.appendChild(this.loader); - - /** - * @summary Animation canvas - * @member {HTMLCanvasElement} - * @readonly - * @private - */ - this.canvas = document.createElement('canvas'); - this.canvas.className = 'psv-loader-canvas'; - - this.canvas.width = this.loader.clientWidth * SYSTEM.pixelRatio; - this.canvas.height = this.loader.clientWidth * SYSTEM.pixelRatio; - this.loader.appendChild(this.canvas); - - /** - * @override - * @property {number} thickness - * @property {string} current - */ - this.prop = { - ...this.prop, - tickness: (this.loader.offsetWidth - this.loader.clientWidth) / 2 * SYSTEM.pixelRatio, - current : null, - }; - - this.refreshUi(); - this.hide(); - } - - /** - * @override - */ - destroy() { - delete this.loader; - delete this.canvas; - - super.destroy(); - } - - /** - * @override - */ - refreshUi() { - if (this.prop.current !== (this.psv.config.loadingImg || this.psv.config.loadingTxt)) { - if (this.prop.current) { - this.loader.removeChild(this.loader.lastChild); - } - - let inner; - if (this.psv.config.loadingImg) { - inner = document.createElement('img'); - inner.className = 'psv-loader-image'; - inner.src = this.psv.config.loadingImg; - } - else if (this.psv.config.loadingTxt) { - inner = document.createElement('div'); - inner.className = 'psv-loader-text'; - inner.innerHTML = this.psv.config.loadingTxt; - } - if (inner) { - const size = Math.round(Math.sqrt(2 * Math.pow((this.canvas.width / 2 - this.prop.tickness / 2) / SYSTEM.pixelRatio, 2))); - inner.style.maxWidth = size + 'px'; - inner.style.maxHeight = size + 'px'; - this.loader.appendChild(inner); - } - - this.prop.current = this.psv.config.loadingImg || this.psv.config.loadingTxt; - } - } - - /** - * @summary Sets the loader progression - * @param {number} value - from 0 to 100 - */ - setProgress(value) { - const context = this.canvas.getContext('2d'); - - context.clearRect(0, 0, this.canvas.width, this.canvas.height); - - context.lineWidth = this.prop.tickness; - context.strokeStyle = getStyle(this.loader, 'color'); - - context.beginPath(); - context.arc( - this.canvas.width / 2, this.canvas.height / 2, - this.canvas.width / 2 - this.prop.tickness / 2, - -Math.PI / 2, MathUtils.clamp(value, 0, 100) / 100 * 2 * Math.PI - Math.PI / 2 - ); - context.stroke(); - - this.psv.trigger(EVENTS.LOAD_PROGRESS, Math.round(value)); - } - -} diff --git a/src/components/Navbar.js b/src/components/Navbar.js deleted file mode 100644 index 40b5d85be..000000000 --- a/src/components/Navbar.js +++ /dev/null @@ -1,271 +0,0 @@ -import { AutorotateButton } from '../buttons/AutorotateButton'; -import { CustomButton } from '../buttons/CustomButton'; -import { DescriptionButton } from '../buttons/DescriptionButton'; -import { DownloadButton } from '../buttons/DownloadButton'; -import { FullscreenButton } from '../buttons/FullscreenButton'; -import { MenuButton } from '../buttons/MenuButton'; -import { MoveDownButton } from '../buttons/MoveDownButton'; -import { MoveLeftButton } from '../buttons/MoveLeftButton'; -import { MoveRightButton } from '../buttons/MoveRightButton'; -import { MoveUpButton } from '../buttons/MoveUpButton'; -import { ZoomInButton } from '../buttons/ZoomInButton'; -import { ZoomOutButton } from '../buttons/ZoomOutButton'; -import { ZoomRangeButton } from '../buttons/ZoomRangeButton'; -import { DEFAULTS } from '../data/config'; -import { PSVError } from '../PSVError'; -import { clone, logWarn } from '../utils'; -import { AbstractComponent } from './AbstractComponent'; -import { NavbarCaption } from './NavbarCaption'; - -/** - * @summary List of available buttons - * @type {Object>} - * @private - */ -const AVAILABLE_BUTTONS = {}; - -/** - * @summary List of available buttons - * @type {Object>>} - * @private - */ -const AVAILABLE_GROUPS = {}; - -/** - * @summary Register a new button available for all viewers - * @param {Class} button - * @param {'start' | 'end' | '[id]:left' | '[id]:right'} [defaultPosition] - * If provided the default configuration of the navbar will be modified. - * @memberOf PSV - */ -export function registerButton(button, defaultPosition) { - if (!button.id) { - throw new PSVError('Button ID is required'); - } - - AVAILABLE_BUTTONS[button.id] = button; - - if (button.groupId) { - AVAILABLE_GROUPS[button.groupId] = AVAILABLE_GROUPS[button.groupId] || []; - AVAILABLE_GROUPS[button.groupId].push(button); - } - - if (typeof defaultPosition === 'string') { - switch (defaultPosition) { - case 'start': - DEFAULTS.navbar.unshift(button.id); - break; - case 'end': - DEFAULTS.navbar.push(button.id); - break; - default: - const [id, pos] = defaultPosition.split(':'); - DEFAULTS.navbar.splice(DEFAULTS.navbar.indexOf(id) + (pos === 'right' ? 1 : 0), 0, button.id); - } - } -} - -[ - AutorotateButton, - ZoomOutButton, - ZoomRangeButton, - ZoomInButton, - DescriptionButton, - DownloadButton, - FullscreenButton, - MoveLeftButton, - MoveRightButton, - MoveUpButton, - MoveDownButton, -].forEach(registerButton); - -/** - * @summary Navigation bar component - * @extends PSV.components.AbstractComponent - * @memberof PSV.components - */ -export class Navbar extends AbstractComponent { - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv, 'psv-navbar psv--capture-event'); - - /** - * @summary List of buttons of the navbar - * @member {PSV.buttons.AbstractButton[]} - * @override - */ - this.children = []; - - /** - * @summary List of collapsed buttons - * @member {PSV.buttons.AbstractButton[]} - * @private - */ - this.collapsed = []; - } - - /** - * @summary Change the buttons visible on the navbar - * @param {string|Array} buttons - * @throws {PSV.PSVError} when a button is unknown - */ - setButtons(buttons) { - this.children.slice().forEach(item => item.destroy()); - this.children.length = 0; - - const cleanedButtons = this.__cleanButtons(buttons); - - // force description button if caption is present (used on narrow screens) - if (cleanedButtons.indexOf(NavbarCaption.id) !== -1 && cleanedButtons.indexOf(DescriptionButton.id) === -1) { - cleanedButtons.splice(cleanedButtons.indexOf(NavbarCaption.id), 0, DescriptionButton.id); - } - - /* eslint-disable no-new */ - cleanedButtons.forEach((button) => { - if (typeof button === 'object') { - new CustomButton(this, button); - } - else if (AVAILABLE_BUTTONS[button]) { - new AVAILABLE_BUTTONS[button](this); - } - else if (AVAILABLE_GROUPS[button]) { - AVAILABLE_GROUPS[button].forEach(buttonCtor => new buttonCtor(this)); // eslint-disable-line new-cap - } - else if (button === NavbarCaption.id) { - new NavbarCaption(this, this.psv.config.caption); - } - else { - throw new PSVError('Unknown button ' + button); - } - }); - - new MenuButton(this); - /* eslint-enable no-new */ - - this.children.forEach((item) => { - if (typeof item.checkSupported === 'function') { - item.checkSupported(); - } - }); - } - - /** - * @summary Sets the bar caption - * @param {string} html - */ - setCaption(html) { - const caption = this.getButton(NavbarCaption.id, false); - caption?.setCaption(html); - } - - /** - * @summary Returns a button by its identifier - * @param {string} id - * @param {boolean} [warnNotFound=true] - * @returns {PSV.buttons.AbstractButton} - */ - getButton(id, warnNotFound = true) { - let button = null; - - this.children.some((item) => { - if (item.prop.id === id) { - button = item; - return true; - } - else { - return false; - } - }); - - if (!button && warnNotFound) { - logWarn(`button "${id}" not found in the navbar`); - } - - return button; - } - - /** - * @summary Shows the navbar - */ - show() { - this.container.classList.add('psv-navbar--open'); - this.prop.visible = true; - } - - /** - * @summary Hides the navbar - */ - hide() { - this.container.classList.remove('psv-navbar--open'); - this.prop.visible = false; - } - - /** - * @override - */ - refreshUi() { - super.refreshUi(); - - if (this.psv.prop.uiRefresh === true) { - const availableWidth = this.container.offsetWidth; - - let totalWidth = 0; - const visibleButtons = []; - const collapsableButtons = []; - - this.children.forEach((item) => { - if (item.prop.visible) { - totalWidth += item.prop.width; - visibleButtons.push(item); - if (item.prop.collapsable) { - collapsableButtons.push(item); - } - } - }); - - if (!visibleButtons.length) { - return; - } - - if (availableWidth < totalWidth && collapsableButtons.length > 0) { - collapsableButtons.forEach(item => item.collapse()); - this.collapsed = collapsableButtons; - - this.getButton(MenuButton.id).show(false); - } - else if (availableWidth >= totalWidth && this.collapsed.length > 0) { - this.collapsed.forEach(item => item.uncollapse()); - this.collapsed = []; - - this.getButton(MenuButton.id).hide(false); - } - - const caption = this.getButton(NavbarCaption.id, false); - if (caption) { - caption.refreshUi(); - } - } - } - - /** - * @summary Ensure the buttons configuration is correct - * @private - */ - __cleanButtons(buttons) { - // true becomes the default array - if (buttons === true) { - return clone(DEFAULTS.navbar); - } - // can be a space or coma separated list - else if (typeof buttons === 'string') { - return buttons.split(/[ ,]/); - } - else { - return buttons || []; - } - } - -} diff --git a/src/components/NavbarCaption.js b/src/components/NavbarCaption.js deleted file mode 100644 index c0ca5db77..000000000 --- a/src/components/NavbarCaption.js +++ /dev/null @@ -1,99 +0,0 @@ -import { DescriptionButton } from '../buttons/DescriptionButton'; -import { AbstractComponent } from './AbstractComponent'; - -/** - * @summary Navbar caption class - * @extends PSV.components.AbstractComponent - * @memberof PSV.components - */ -export class NavbarCaption extends AbstractComponent { - - static id = 'caption'; - - /** - * @param {PSV.components.Navbar} navbar - * @param {string} caption - */ - constructor(navbar, caption) { - super(navbar, 'psv-caption'); - - /** - * @override - * @property {string} id - * @property {boolean} collapsable - * @property {number} width - * @property {number} contentWidth - width of the caption content - */ - this.prop = { - ...this.prop, - id : this.constructor.id, - collapsable : false, - width : 0, - contentWidth: 0, - }; - - /** - * @member {HTMLElement} - * @readonly - * @private - */ - this.content = document.createElement('div'); - this.content.className = 'psv-caption-content'; - this.container.appendChild(this.content); - - this.setCaption(caption); - } - - /** - * @override - */ - destroy() { - delete this.content; - - super.destroy(); - } - - /** - * @summary Sets the bar caption - * @param {string} html - */ - setCaption(html) { - this.show(); - this.content.innerHTML = html; - this.prop.contentWidth = html ? this.content.offsetWidth : 0; - this.refreshUi(); - } - - /** - * @summary Toggles content and icon depending on available space - * @private - */ - refreshUi() { - this.toggle(this.container.offsetWidth >= this.prop.contentWidth); - this.__refreshButton(); - } - - /** - * @override - */ - hide() { - this.content.style.display = 'none'; - this.prop.visible = false; - } - - /** - * @override - */ - show() { - this.content.style.display = ''; - this.prop.visible = true; - } - - /** - * @private - */ - __refreshButton() { - this.psv.navbar.getButton(DescriptionButton.id, false)?.refreshUi(true); - } - -} diff --git a/src/components/Notification.js b/src/components/Notification.js deleted file mode 100644 index b11295cbb..000000000 --- a/src/components/Notification.js +++ /dev/null @@ -1,122 +0,0 @@ -import { EVENTS } from '../data/constants'; -import { PSVError } from '../PSVError'; -import { AbstractComponent } from './AbstractComponent'; - -/** - * @summary Notification component - * @extends PSV.components.AbstractComponent - * @memberof PSV.components - */ -export class Notification extends AbstractComponent { - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv, 'psv-notification'); - - /** - * @override - * @property {*} timeout - */ - this.prop = { - ...this.prop, - visible : false, - contentId: undefined, - timeout : null, - }; - - /** - * Notification content - * @member {HTMLElement} - * @readonly - * @private - */ - this.content = document.createElement('div'); - this.content.className = 'psv-notification-content'; - this.container.appendChild(this.content); - - this.content.addEventListener('click', () => this.hide()); - } - - /** - * @override - */ - destroy() { - delete this.content; - - super.destroy(); - } - - /** - * @override - * @param {string} [id] - */ - isVisible(id) { - return this.prop.visible && (!id || !this.prop.contentId || this.prop.contentId === id); - } - - /** - * @override - * @summary This method is not supported - * @throws {PSV.PSVError} always - */ - toggle() { - throw new PSVError('Notification cannot be toggled'); - } - - /** - * @summary Displays a notification on the viewer - * @param {Object|string} config - * @param {string} [config.id] - unique identifier to use with "hide" - * @param {string} config.content - * @param {number} [config.timeout] - * @fires PSV.show-notification - * - * @example - * viewer.showNotification({ content: 'Hello world', timeout: 5000 }) - * @example - * viewer.showNotification('Hello world') - */ - show(config) { - if (this.prop.timeout) { - clearTimeout(this.prop.timeout); - this.prop.timeout = null; - } - - if (typeof config === 'string') { - config = { content: config }; - } - - this.prop.contentId = config.id; - this.content.innerHTML = config.content; - - this.container.classList.add('psv-notification--visible'); - this.prop.visible = true; - - this.psv.trigger(EVENTS.SHOW_NOTIFICATION, config.id); - - if (config.timeout) { - this.prop.timeout = setTimeout(() => this.hide(config.id), config.timeout); - } - } - - /** - * @summary Hides the notification - * @param {string} [id] - * @fires PSV.hide-notification - */ - hide(id) { - if (this.isVisible(id)) { - const contentId = this.prop.contentId; - - this.container.classList.remove('psv-notification--visible'); - this.prop.visible = false; - - this.prop.contentId = undefined; - - this.psv.trigger(EVENTS.HIDE_NOTIFICATION, contentId); - } - } - -} diff --git a/src/components/Overlay.js b/src/components/Overlay.js deleted file mode 100644 index b8dfa17db..000000000 --- a/src/components/Overlay.js +++ /dev/null @@ -1,163 +0,0 @@ -import { EVENTS, KEY_CODES } from '../data/constants'; -import { PSVError } from '../PSVError'; -import { AbstractComponent } from './AbstractComponent'; - -/** - * @summary Overlay component - * @extends PSV.components.AbstractComponent - * @memberof PSV.components - */ -export class Overlay extends AbstractComponent { - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv, 'psv-overlay'); - - /** - * @override - * @property {string} contentId - * @property {boolean} dissmisable - */ - this.prop = { - ...this.prop, - contentId : undefined, - dissmisable: true, - }; - - /** - * Image container - * @member {HTMLElement} - * @readonly - * @private - */ - this.image = document.createElement('div'); - this.image.className = 'psv-overlay-image'; - this.container.appendChild(this.image); - - /** - * Text container - * @member {HTMLElement} - * @readonly - * @private - */ - this.text = document.createElement('div'); - this.text.className = 'psv-overlay-text'; - this.container.appendChild(this.text); - - /** - * Subtext container - * @member {HTMLElement} - * @readonly - * @private - */ - this.subtext = document.createElement('div'); - this.subtext.className = 'psv-overlay-subtext'; - this.container.appendChild(this.subtext); - - this.psv.on(EVENTS.CLICK, this); - this.psv.on(EVENTS.KEY_PRESS, this); - - super.hide(); - } - - /** - * @override - */ - destroy() { - this.psv.off(EVENTS.CLICK, this); - this.psv.off(EVENTS.KEY_PRESS, this); - - delete this.image; - delete this.text; - delete this.subtext; - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case EVENTS.CLICK: - if (this.isVisible() && this.prop.dissmisable) { - this.hide(); - e.stopPropagation(); - } - break; - case EVENTS.KEY_PRESS: - if (this.isVisible() && this.prop.dissmisable && e.args[0] === KEY_CODES.Escape) { - this.hide(); - e.preventDefault(); - } - break; - } - /* eslint-enable */ - } - - /** - * @override - * @param {string} [id] - */ - isVisible(id) { - return this.prop.visible && (!id || !this.prop.contentId || this.prop.contentId === id); - } - - /** - * @override - * @summary This method is not supported - * @throws {PSV.PSVError} always - */ - toggle() { - throw new PSVError('Overlay cannot be toggled'); - } - - /** - * @summary Displays an overlay on the viewer - * @param {Object|string} config - * @param {string} [config.id] - unique identifier to use with "hide" - * @param {string} config.image - SVG image/icon displayed above the text - * @param {string} config.text - main message - * @param {string} [config.subtext] - secondary message - * @param {boolean} [config.dissmisable=true] - if the user can hide the overlay by clicking - * @fires PSV.show-overlay - */ - show(config) { - if (typeof config === 'string') { - config = { text: config }; - } - - this.prop.contentId = config.id; - this.prop.dissmisable = config.dissmisable !== false; - this.image.innerHTML = config.image || ''; - this.text.innerHTML = config.text || ''; - this.subtext.innerHTML = config.subtext || ''; - - super.show(); - - this.psv.trigger(EVENTS.SHOW_OVERLAY, config.id); - } - - /** - * @summary Hides the overlay - * @param {string} [id] - * @fires PSV.hide-overlay - */ - hide(id) { - if (this.isVisible(id)) { - const contentId = this.prop.contentId; - - super.hide(); - - this.prop.contentId = undefined; - - this.psv.trigger(EVENTS.HIDE_OVERLAY, contentId); - } - } - -} diff --git a/src/components/Panel.js b/src/components/Panel.js deleted file mode 100644 index d4f90e709..000000000 --- a/src/components/Panel.js +++ /dev/null @@ -1,319 +0,0 @@ -import { EVENTS, KEY_CODES } from '../data/constants'; -import { SYSTEM } from '../data/system'; -import { PSVError } from '../PSVError'; -import { toggleClass } from '../utils'; -import { AbstractComponent } from './AbstractComponent'; - -/** - * @summary Minimum width of the panel - * @type {number} - * @constant - * @private - */ -const PANEL_MIN_WIDTH = 200; - -/** - * @summary Panel component - * @extends PSV.components.AbstractComponent - * @memberof PSV.components - */ -export class Panel extends AbstractComponent { - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv, 'psv-panel psv--capture-event'); - - /** - * @override - * @property {string} contentId - * @property {number} mouseX - * @property {number} mouseY - * @property {boolean} mousedown - * @property {function} clickHandler - * @property {function} keyHandler - */ - this.prop = { - ...this.prop, - visible : false, - contentId : undefined, - mouseX : 0, - mouseY : 0, - mousedown : false, - clickHandler: null, - keyHandler : null, - width : {}, - }; - - const resizer = document.createElement('div'); - resizer.className = 'psv-panel-resizer'; - this.container.appendChild(resizer); - - const closeBtn = document.createElement('div'); - closeBtn.className = 'psv-panel-close-button'; - this.container.appendChild(closeBtn); - - /** - * @summary Content container - * @member {HTMLElement} - * @readonly - * @private - */ - this.content = document.createElement('div'); - this.content.className = 'psv-panel-content'; - this.container.appendChild(this.content); - - // Stop wheel event bubbling from panel - this.container.addEventListener(SYSTEM.mouseWheelEvent, e => e.stopPropagation()); - - closeBtn.addEventListener('click', () => this.hide()); - - // Event for panel resizing + stop bubling - resizer.addEventListener('mousedown', this); - resizer.addEventListener('touchstart', this); - this.psv.container.addEventListener('mouseup', this); - this.psv.container.addEventListener('touchend', this); - this.psv.container.addEventListener('mousemove', this); - this.psv.container.addEventListener('touchmove', this); - - this.psv.on(EVENTS.KEY_PRESS, this); - } - - /** - * @override - */ - destroy() { - this.psv.off(EVENTS.KEY_PRESS, this); - - this.psv.container.removeEventListener('mousemove', this); - this.psv.container.removeEventListener('touchmove', this); - this.psv.container.removeEventListener('mouseup', this); - this.psv.container.removeEventListener('touchend', this); - - delete this.prop; - delete this.content; - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case 'mousedown': this.__onMouseDown(e); break; - case 'touchstart': this.__onTouchStart(e); break; - case 'mousemove': this.__onMouseMove(e); break; - case 'touchmove': this.__onTouchMove(e); break; - case 'mouseup': this.__onMouseUp(e); break; - case 'touchend': this.__onMouseUp(e); break; - case EVENTS.KEY_PRESS: - if (this.isVisible() && e.args[0] === KEY_CODES.Escape) { - this.hide(); - e.preventDefault(); - } - break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @override - * @param {string} [id] - */ - isVisible(id) { - return this.prop.visible && (!id || !this.prop.contentId || this.prop.contentId === id); - } - - /** - * @override - * @summary This method is not supported - * @throws {PSV.PSVError} always - */ - toggle() { - throw new PSVError('Panel cannot be toggled'); - } - - /** - * @summary Shows the panel - * @param {string|Object} config - * @param {string} [config.id] - unique identifier to use with "hide" and to store the user desired width - * @param {string} config.content - HTML content of the panel - * @param {boolean} [config.noMargin=false] - remove the default margins - * @param {string} [config.width] - initial width - * @param {Function} [config.clickHandler] - called when the user clicks inside the panel or presses the Enter key while an element focused - * @fires PSV.open-panel - */ - show(config) { - const wasVisible = this.isVisible(config.id); - - if (typeof config === 'string') { - config = { content: config }; - } - - this.prop.contentId = config.id; - this.prop.visible = true; - - if (this.prop.clickHandler) { - this.content.removeEventListener('click', this.prop.clickHandler); - this.content.removeEventListener('keydown', this.prop.keyHandler); - this.prop.clickHandler = null; - this.prop.keyHandler = null; - } - - if (config.id && this.prop.width[config.id]) { - this.container.style.width = this.prop.width[config.id]; - } - else if (config.width) { - this.container.style.width = config.width; - } - else { - this.container.style.width = null; - } - - this.content.innerHTML = config.content; - this.content.scrollTop = 0; - this.container.classList.add('psv-panel--open'); - - toggleClass(this.content, 'psv-panel-content--no-margin', config.noMargin === true); - - if (config.clickHandler) { - this.prop.clickHandler = config.clickHandler; - this.prop.keyHandler = (e) => { - if (e.key === KEY_CODES.Enter) { - config.clickHandler(e); - } - }; - this.content.addEventListener('click', this.prop.clickHandler); - this.content.addEventListener('keydown', this.prop.keyHandler); - - // focus the first element if possible, after animation ends - if (!wasVisible) { - setTimeout(() => { - this.content.querySelector('a,button,[tabindex]')?.focus(); - }, 300); - } - } - - this.psv.trigger(EVENTS.OPEN_PANEL, config.id); - } - - /** - * @summary Hides the panel - * @param {string} [id] - * @fires PSV.close-panel - */ - hide(id) { - if (this.isVisible(id)) { - const contentId = this.prop.contentId; - - this.prop.visible = false; - this.prop.contentId = undefined; - - this.content.innerHTML = null; - this.container.classList.remove('psv-panel--open'); - - if (this.prop.clickHandler) { - this.content.removeEventListener('click', this.prop.clickHandler); - this.prop.clickHandler = null; - } - - this.psv.trigger(EVENTS.CLOSE_PANEL, contentId); - } - } - - /** - * @summary Handles mouse down events - * @param {MouseEvent} evt - * @private - */ - __onMouseDown(evt) { - evt.stopPropagation(); - this.__startResize(evt); - } - - /** - * @summary Handles touch events - * @param {TouchEvent} evt - * @private - */ - __onTouchStart(evt) { - evt.stopPropagation(); - this.__startResize(evt.changedTouches[0]); - } - - /** - * @summary Handles mouse up events - * @param {MouseEvent} evt - * @private - */ - __onMouseUp(evt) { - if (this.prop.mousedown) { - evt.stopPropagation(); - this.prop.mousedown = false; - this.content.classList.remove('psv-panel-content--no-interaction'); - } - } - - /** - * @summary Handles mouse move events - * @param {MouseEvent} evt - * @private - */ - __onMouseMove(evt) { - if (this.prop.mousedown) { - evt.stopPropagation(); - this.__resize(evt); - } - } - - /** - * @summary Handles touch move events - * @param {TouchEvent} evt - * @private - */ - __onTouchMove(evt) { - if (this.prop.mousedown) { - this.__resize(evt.touches[0]); - } - } - - /** - * @summary Initializes the panel resize - * @param {MouseEvent|Touch} evt - * @private - */ - __startResize(evt) { - this.prop.mouseX = evt.clientX; - this.prop.mouseY = evt.clientY; - this.prop.mousedown = true; - this.content.classList.add('psv-panel-content--no-interaction'); - } - - /** - * @summary Resizes the panel - * @param {MouseEvent|Touch} evt - * @private - */ - __resize(evt) { - const x = evt.clientX; - const y = evt.clientY; - const width = Math.max(PANEL_MIN_WIDTH, this.container.offsetWidth - (x - this.prop.mouseX)) + 'px'; - - if (this.prop.contentId) { - this.prop.width[this.prop.contentId] = width; - } - - this.container.style.width = width; - - this.prop.mouseX = x; - this.prop.mouseY = y; - } - -} diff --git a/src/components/Tooltip.js b/src/components/Tooltip.js deleted file mode 100644 index 02e969383..000000000 --- a/src/components/Tooltip.js +++ /dev/null @@ -1,398 +0,0 @@ -import { EVENTS } from '../data/constants'; -import { PSVError } from '../PSVError'; -import { addClasses, cleanPosition, positionIsOrdered } from '../utils'; -import { AbstractComponent } from './AbstractComponent'; - -const STATE = { NONE: 0, SHOWING: 1, HIDING: 2, READY: 3 }; - -/** - * @typedef {Object} PSV.components.Tooltip.Position - * @summary Object defining the tooltip position - * @property {number} top - Position of the tip of the arrow of the tooltip, in pixels - * @property {number} left - Position of the tip of the arrow of the tooltip, in pixels - * @property {string|string[]} [position='top center'] - Tooltip position toward it's arrow tip. - * Accepted values are combinations of `top`, `center`, `bottom` and `left`, `center`, `right` - * @property {Object} [box] - Used when displaying a tooltip on a marker - * @property {number} [box.width=0] - * @property {number} [box.height=0] - */ - -/** - * @typedef {PSV.components.Tooltip.Position} PSV.components.Tooltip.Config - * @summary Object defining the tooltip configuration - * @property {string} content - HTML content of the tooltip - * @property {string} [className] - Additional CSS class added to the tooltip - * @property {*} [data] - Userdata associated to the tooltip - */ - -/** - * @summary Tooltip component - * @description Never instanciate tooltips directly use {@link PSV.services.TooltipRenderer} instead - * @extends PSV.components.AbstractComponent - * @memberof PSV.components - */ -export class Tooltip extends AbstractComponent { - - /** - * @param {PSV.Viewer} psv - * @param {{arrow: number, border: number}} size - */ - constructor(psv, size) { - super(psv, 'psv-tooltip'); - - /** - * @override - * @property {number} arrow - * @property {number} border - * @property {number} width - * @property {number} height - * @property {string} pos - * @property {string} state - * @property {*} data - */ - this.prop = { - ...this.prop, - ...size, - state : STATE.NONE, - width : 0, - height: 0, - pos : '', - config: null, - data : null, - }; - - /** - * Tooltip content - * @member {HTMLElement} - * @readonly - * @private - */ - this.content = document.createElement('div'); - this.content.className = 'psv-tooltip-content'; - this.container.appendChild(this.content); - - /** - * Tooltip arrow - * @member {HTMLElement} - * @readonly - * @package - */ - this.arrow = document.createElement('div'); - this.arrow.className = 'psv-tooltip-arrow'; - this.container.appendChild(this.arrow); - - this.container.addEventListener('transitionend', this); - - this.container.style.top = '-1000px'; - this.container.style.left = '-1000px'; - } - - /** - * @override - */ - destroy() { - delete this.arrow; - delete this.content; - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case 'transitionend': this.__onTransitionEnd(e); break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @override - * @summary This method is not supported - * @throws {PSV.PSVError} always - */ - toggle() { - throw new PSVError('Tooltip cannot be toggled'); - } - - /** - * @summary Displays the tooltip on the viewer - * Do not call this method directly, use {@link PSV.services.TooltipRenderer} instead. - * @param {PSV.components.Tooltip.Config} config - * - * @fires PSV.show-tooltip - * @throws {PSV.PSVError} when the configuration is incorrect - * - * @package - */ - show(config) { - if (this.prop.state !== STATE.NONE) { - throw new PSVError('Initialized tooltip cannot be re-initialized'); - } - - if (config.className) { - addClasses(this.container, config.className); - } - - this.content.innerHTML = config.content; - - const rect = this.container.getBoundingClientRect(); - this.prop.width = rect.right - rect.left; - this.prop.height = rect.bottom - rect.top; - - this.prop.state = STATE.READY; - - this.move(config); - - this.prop.data = config.data; - this.prop.state = STATE.SHOWING; - - this.psv.trigger(EVENTS.SHOW_TOOLTIP, this.prop.data, this); - - this.__waitImages(); - } - - /** - * @summary Moves the tooltip to a new position - * @param {PSV.components.Tooltip.Position} config - * - * @throws {PSV.PSVError} when the configuration is incorrect - */ - move(config) { - if (this.prop.state !== STATE.SHOWING && this.prop.state !== STATE.READY) { - throw new PSVError('Uninitialized tooltip cannot be moved'); - } - - if (!config.box) { - config.box = { - width : 0, - height: 0, - }; - } - - this.config = config; - - const t = this.container; - const a = this.arrow; - - // compute size - const style = { - posClass : cleanPosition(config.position, { allowCenter: false, cssOrder: false }) || ['top', 'center'], - width : this.prop.width, - height : this.prop.height, - top : 0, - left : 0, - arrowTop : 0, - arrowLeft: 0, - }; - - // set initial position - this.__computeTooltipPosition(style, config); - - // correct position if overflow - let swapY = null; - let swapX = null; - if (style.top < 0) { - swapY = 'bottom'; - } - else if (style.top + style.height > this.psv.prop.size.height) { - swapY = 'top'; - } - if (style.left < 0) { - swapX = 'right'; - } - else if (style.left + style.width > this.psv.prop.size.width) { - swapX = 'left'; - } - if (swapX || swapY) { - const ordered = positionIsOrdered(style.posClass); - if (swapY) { - style.posClass[ordered ? 0 : 1] = swapY; - } - if (swapX) { - style.posClass[ordered ? 1 : 0] = swapX; - } - this.__computeTooltipPosition(style, config); - } - - // apply position - t.style.top = style.top + 'px'; - t.style.left = style.left + 'px'; - - a.style.top = style.arrowTop + 'px'; - a.style.left = style.arrowLeft + 'px'; - - const newPos = style.posClass.join('-'); - if (newPos !== this.prop.pos) { - t.classList.remove(`psv-tooltip--${this.prop.pos}`); - - this.prop.pos = newPos; - t.classList.add(`psv-tooltip--${this.prop.pos}`); - } - } - - /** - * @summary Hides the tooltip - * @fires PSV.hide-tooltip - */ - hide() { - this.container.classList.remove('psv-tooltip--visible'); - this.prop.state = STATE.HIDING; - - this.psv.trigger(EVENTS.HIDE_TOOLTIP, this.prop.data); - } - - /** - * @summary Finalize transition - * @param {TransitionEvent} e - * @private - */ - __onTransitionEnd(e) { - if (e.propertyName === 'transform') { - switch (this.prop.state) { - case STATE.SHOWING: - this.container.classList.add('psv-tooltip--visible'); - this.prop.state = STATE.READY; - break; - - case STATE.HIDING: - this.prop.state = STATE.NONE; - this.destroy(); - break; - - default: - // nothing - } - } - } - - /** - * @summary Computes the position of the tooltip and its arrow - * @param {Object} style - * @param {Object} config - * @private - */ - __computeTooltipPosition(style, config) { - const arrow = this.prop.arrow; - const top = config.top; - const height = style.height; - const left = config.left; - const width = style.width; - const offsetSide = arrow + this.prop.border; - const offsetX = config.box.width / 2 + arrow * 2; - const offsetY = config.box.height / 2 + arrow * 2; - - switch (style.posClass.join('-')) { - case 'top-left': - style.top = top - offsetY - height; - style.left = left + offsetSide - width; - style.arrowTop = height; - style.arrowLeft = width - offsetSide - arrow; - break; - case 'top-center': - style.top = top - offsetY - height; - style.left = left - width / 2; - style.arrowTop = height; - style.arrowLeft = width / 2 - arrow; - break; - case 'top-right': - style.top = top - offsetY - height; - style.left = left - offsetSide; - style.arrowTop = height; - style.arrowLeft = arrow; - break; - case 'bottom-left': - style.top = top + offsetY; - style.left = left + offsetSide - width; - style.arrowTop = -arrow * 2; - style.arrowLeft = width - offsetSide - arrow; - break; - case 'bottom-center': - style.top = top + offsetY; - style.left = left - width / 2; - style.arrowTop = -arrow * 2; - style.arrowLeft = width / 2 - arrow; - break; - case 'bottom-right': - style.top = top + offsetY; - style.left = left - offsetSide; - style.arrowTop = -arrow * 2; - style.arrowLeft = arrow; - break; - case 'left-top': - style.top = top + offsetSide - height; - style.left = left - offsetX - width; - style.arrowTop = height - offsetSide - arrow; - style.arrowLeft = width; - break; - case 'center-left': - style.top = top - height / 2; - style.left = left - offsetX - width; - style.arrowTop = height / 2 - arrow; - style.arrowLeft = width; - break; - case 'left-bottom': - style.top = top - offsetSide; - style.left = left - offsetX - width; - style.arrowTop = arrow; - style.arrowLeft = width; - break; - case 'right-top': - style.top = top + offsetSide - height; - style.left = left + offsetX; - style.arrowTop = height - offsetSide - arrow; - style.arrowLeft = -arrow * 2; - break; - case 'center-right': - style.top = top - height / 2; - style.left = left + offsetX; - style.arrowTop = height / 2 - arrow; - style.arrowLeft = -arrow * 2; - break; - case 'right-bottom': - style.top = top - offsetSide; - style.left = left + offsetX; - style.arrowTop = arrow; - style.arrowLeft = -arrow * 2; - break; - - // no default - } - } - - /** - * @summary If the tooltip contains images, recompute its size once they are loaded - * @private - */ - __waitImages() { - const images = this.content.querySelectorAll('img'); - - if (images.length > 0) { - const promises = []; - - images.forEach((image) => { - promises.push(new Promise((resolve) => { - image.onload = resolve; - image.onerror = resolve; - })); - }); - - Promise.all(promises) - .then(() => { - if (this.prop.state === STATE.SHOWING || this.prop.state === STATE.READY) { - const rect = this.container.getBoundingClientRect(); - this.prop.width = rect.right - rect.left; - this.prop.height = rect.bottom - rect.top; - this.move(this.config); - } - }); - } - } - -} diff --git a/src/data/config.js b/src/data/config.js deleted file mode 100644 index 24e407bee..000000000 --- a/src/data/config.js +++ /dev/null @@ -1,268 +0,0 @@ -import { MathUtils } from 'three'; -import { AbstractAdapter } from '../adapters/AbstractAdapter'; -import { EquirectangularAdapter } from '../adapters/equirectangular'; -import { AbstractPlugin } from '../plugins/AbstractPlugin'; -import { PSVError } from '../PSVError'; -import { clone, deepmerge, each, isNil, logWarn, parseAngle, parseSpeed, pluginInterop } from '../utils'; -import { ACTIONS, KEY_CODES } from './constants'; - -/** - * @summary Default options - * @type {PSV.Options} - * @memberOf PSV - * @constant - */ -export const DEFAULTS = { - panorama : null, - overlay : null, - overlayOpacity : 1, - container : null, - adapter : null, - plugins : [], - caption : null, - description : null, - downloadUrl : null, - loadingImg : null, - loadingTxt : 'Loading...', - size : null, - fisheye : false, - minFov : 30, - maxFov : 90, - defaultZoomLvl : 50, - defaultLong : 0, - defaultLat : 0, - sphereCorrection : null, - moveSpeed : 1, - zoomSpeed : 1, - autorotateDelay : null, - autorotateIdle : false, - autorotateSpeed : '2rpm', - autorotateLat : null, - autorotateZoomLvl : null, - moveInertia : true, - mousewheel : true, - mousemove : true, - mousewheelCtrlKey : false, - touchmoveTwoFingers: false, - useXmpData : true, - panoData : null, - requestHeaders : null, - canvasBackground : '#000', - withCredentials : false, - navbar : [ - 'autorotate', - 'zoom', - 'move', - 'download', - 'description', - 'caption', - 'fullscreen', - ], - lang : { - autorotate: 'Automatic rotation', - zoom : 'Zoom', - zoomOut : 'Zoom out', - zoomIn : 'Zoom in', - move : 'Move', - download : 'Download', - fullscreen: 'Fullscreen', - menu : 'Menu', - twoFingers: 'Use two fingers to navigate', - ctrlZoom : 'Use ctrl + scroll to zoom the image', - loadError : 'The panorama can\'t be loaded', - }, - keyboard : { - [KEY_CODES.ArrowUp] : ACTIONS.ROTATE_LAT_UP, - [KEY_CODES.ArrowDown] : ACTIONS.ROTATE_LAT_DOWN, - [KEY_CODES.ArrowRight]: ACTIONS.ROTATE_LONG_RIGHT, - [KEY_CODES.ArrowLeft] : ACTIONS.ROTATE_LONG_LEFT, - [KEY_CODES.PageUp] : ACTIONS.ZOOM_IN, - [KEY_CODES.PageDown] : ACTIONS.ZOOM_OUT, - [KEY_CODES.Plus] : ACTIONS.ZOOM_IN, - [KEY_CODES.Minus] : ACTIONS.ZOOM_OUT, - [KEY_CODES.Space] : ACTIONS.TOGGLE_AUTOROTATE, - }, -}; - -/** - * @summary List of unmodifiable options and their error messages - * @private - */ -export const READONLY_OPTIONS = { - panorama : 'Use setPanorama method to change the panorama', - panoData : 'Use setPanorama method to change the panorama', - container: 'Cannot change viewer container', - adapter : 'Cannot change adapter', - plugins : 'Cannot change plugins', -}; - -/** - * @summary List of deprecated options and their warning messages - * @private - */ -export const DEPRECATED_OPTIONS = { - captureCursor: 'captureCursor is deprecated', -}; - -/** - * @summary Parsers/validators for each option - * @private - */ -export const CONFIG_PARSERS = { - container : (container) => { - if (!container) { - throw new PSVError('No value given for container.'); - } - return container; - }, - adapter : (adapter) => { - if (!adapter) { - adapter = [EquirectangularAdapter]; - } - else if (Array.isArray(adapter)) { - adapter = [pluginInterop(adapter[0], AbstractAdapter), adapter[1]]; - } - else { - adapter = [pluginInterop(adapter, AbstractAdapter)]; - } - if (!adapter[0]) { - throw new PSVError('Un undefined value with given for adapter.'); - } - return adapter; - }, - overlayOpacity : (overlayOpacity) => { - return MathUtils.clamp(overlayOpacity, 0, 1); - }, - defaultLong : (defaultLong) => { - // defaultLat is between 0 and PI - return parseAngle(defaultLong); - }, - defaultLat : (defaultLat) => { - // defaultLat is between -PI/2 and PI/2 - return parseAngle(defaultLat, true); - }, - defaultZoomLvl : (defaultZoomLvl) => { - return MathUtils.clamp(defaultZoomLvl, 0, 100); - }, - minFov : (minFov, config) => { - // minFov and maxFov must be ordered - if (config.maxFov < minFov) { - logWarn('maxFov cannot be lower than minFov'); - minFov = config.maxFov; - } - // minFov between 1 and 179 - return MathUtils.clamp(minFov, 1, 179); - }, - maxFov : (maxFov, config) => { - // minFov and maxFov must be ordered - if (maxFov < config.minFov) { - maxFov = config.minFov; - } - // maxFov between 1 and 179 - return MathUtils.clamp(maxFov, 1, 179); - }, - lang : (lang) => { - if (Array.isArray(lang.twoFingers)) { - logWarn('lang.twoFingers must not be an array'); - lang.twoFingers = lang.twoFingers[0]; - } - return { - ...DEFAULTS.lang, - ...lang, - }; - }, - keyboard : (keyboard) => { - // keyboard=true becomes the default map - if (keyboard === true) { - return clone(DEFAULTS.keyboard); - } - return keyboard; - }, - autorotateLat : (autorotateLat, config) => { - // default autorotateLat is defaultLat - if (autorotateLat === null) { - return parseAngle(config.defaultLat, true); - } - // autorotateLat is between -PI/2 and PI/2 - else { - return parseAngle(autorotateLat, true); - } - }, - autorotateZoomLvl: (autorotateZoomLvl) => { - if (isNil(autorotateZoomLvl)) { - return null; - } - else { - return MathUtils.clamp(autorotateZoomLvl, 0, 100); - } - }, - autorotateSpeed : (autorotateSpeed) => { - return parseSpeed(autorotateSpeed); - }, - autorotateIdle : (autorotateIdle, config) => { - if (autorotateIdle && isNil(config.autorotateDelay)) { - logWarn('autorotateIdle requires a non null autorotateDelay'); - return false; - } - return autorotateIdle; - }, - fisheye : (fisheye) => { - // translate boolean fisheye to amount - if (fisheye === true) { - return 1; - } - else if (fisheye === false) { - return 0; - } - return fisheye; - }, - plugins : (plugins) => { - return plugins - .map((plugin) => { - if (Array.isArray(plugin)) { - plugin = [pluginInterop(plugin[0], AbstractPlugin), plugin[1]]; - } - else { - plugin = [pluginInterop(plugin, AbstractPlugin)]; - } - if (!plugin[0]) { - throw new PSVError('Un undefined value was given for plugins.'); - } - return plugin; - }); - }, -}; - -/** - * @summary Merge user config with default config and performs validation - * @param {PSV.Options} options - * @returns {PSV.Options} - * @memberOf PSV - * @private - */ -export function getConfig(options) { - const tempConfig = clone(DEFAULTS); - deepmerge(tempConfig, options); - - const config = {}; - - each(tempConfig, (value, key) => { - if (DEPRECATED_OPTIONS[key]) { - logWarn(DEPRECATED_OPTIONS[key]); - return; - } - - if (!Object.prototype.hasOwnProperty.call(DEFAULTS, key)) { - throw new PSVError(`Unknown option ${key}`); - } - - if (CONFIG_PARSERS[key]) { - config[key] = CONFIG_PARSERS[key](value, tempConfig); - } - else { - config[key] = value; - } - }); - - return config; -} diff --git a/src/data/constants.js b/src/data/constants.js deleted file mode 100644 index 105167ecb..000000000 --- a/src/data/constants.js +++ /dev/null @@ -1,399 +0,0 @@ -/** - * @namespace PSV.constants - */ - -/** - * @summary Default duration of the transition between panoramas - * @memberOf PSV.constants - * @type {number} - * @constant - */ -export const DEFAULT_TRANSITION = 1500; - -/** - * @summary Minimum duration of the animations created with {@link Viewer#animate} - * @memberOf PSV.constants - * @type {number} - * @constant - */ -export const ANIMATION_MIN_DURATION = 500; - -/** - * @summary Number of pixels bellow which a mouse move will be considered as a click - * @memberOf PSV.constants - * @type {number} - * @constant - */ -export const MOVE_THRESHOLD = 4; - -/** - * @summary Delay in milliseconds between two clicks to consider a double click - * @memberOf PSV.constants - * @type {number} - * @constant - */ -export const DBLCLICK_DELAY = 300; - -/** - * @summary Delay in milliseconds to emulate a long touch - * @memberOf PSV.constants - * @type {number} - * @constant - */ -export const LONGTOUCH_DELAY = 500; - -/** - * @summary Delay in milliseconds to for the two fingers overlay to appear - * @memberOf PSV.constants - * @type {number} - * @constant - */ -export const TWOFINGERSOVERLAY_DELAY = 100; - -/** - * @summary Duration in milliseconds of the "ctrl zoom" overlay - * @memberOf PSV.constants - * @type {number} - * @constant - */ -export const CTRLZOOM_TIMEOUT = 2000; - -/** - * @summary Time size of the mouse position history used to compute inertia - * @memberOf PSV.constants - * @type {number} - * @constant - */ -export const INERTIA_WINDOW = 300; - -/** - * @summary Radius of the THREE.SphereGeometry, Half-length of the THREE.BoxGeometry - * @memberOf PSV.constants - * @type {number} - * @constant - */ -export const SPHERE_RADIUS = 10; - -/** - * @summary Property name added to viewer element - * @memberOf PSV.constants - * @type {string} - * @constant - */ -export const VIEWER_DATA = 'photoSphereViewer'; - -/** - * @summary Property added the the main Mesh object - * @memberOf PSV.constants - * @type {string} - * @constant - */ -export const MESH_USER_DATA = 'psvSphere'; - -/** - * @summary Available actions - * @memberOf PSV.constants - * @enum {string} - * @constant - */ -export const ACTIONS = { - ROTATE_LAT_UP : 'rotateLatitudeUp', - ROTATE_LAT_DOWN : 'rotateLatitudeDown', - ROTATE_LONG_RIGHT: 'rotateLongitudeRight', - ROTATE_LONG_LEFT : 'rotateLongitudeLeft', - ZOOM_IN : 'zoomIn', - ZOOM_OUT : 'zoomOut', - TOGGLE_AUTOROTATE: 'toggleAutorotate', -}; - -/** - * @summary Available events names - * @memberOf PSV.constants - * @enum {string} - * @constant - */ -export const EVENTS = { - /** - * @event autorotate - * @memberof PSV - * @summary Triggered when the automatic rotation is enabled/disabled - * @param {boolean} enabled - */ - AUTOROTATE : 'autorotate', - /** - * @event before-render - * @memberof PSV - * @summary Triggered before a render, used to modify the view - * @param {number} timestamp - time provided by requestAnimationFrame - * @param {number} elapsed - time elapsed from the previous frame - */ - BEFORE_RENDER : 'before-render', - /** - * @event before-rotate - * @memberOf PSV - * @summary Triggered before a rotate operation, can be cancelled - * @param {PSV.ExtendedPosition} - */ - BEFORE_ROTATE : 'before-rotate', - /** - * @event click - * @memberof PSV - * @summary Triggered when the user clicks on the viewer (everywhere excluding the navbar and the side panel) - * @param {PSV.ClickData} data - */ - CLICK : 'click', - /** - * @event close-panel - * @memberof PSV - * @summary Triggered when the panel is closed - * @param {string} [id] - */ - CLOSE_PANEL : 'close-panel', - /** - * @event config-changed - * @memberOf PSV - * @summary Triggered after a call to setOption/setOptions - * @param {string[]} name of changed options - */ - CONFIG_CHANGED : 'config-changed', - /** - * @event dblclick - * @memberof PSV - * @summary Triggered when the user double clicks on the viewer. The simple `click` event is always fired before `dblclick` - * @param {PSV.ClickData} data - */ - DOUBLE_CLICK : 'dblclick', - /** - * @event fullscreen-updated - * @memberof PSV - * @summary Triggered when the fullscreen mode is enabled/disabled - * @param {boolean} enabled - */ - FULLSCREEN_UPDATED: 'fullscreen-updated', - /** - * @event hide-notification - * @memberof PSV - * @summary Triggered when the notification is hidden - * @param {string} [id] - */ - HIDE_NOTIFICATION : 'hide-notification', - /** - * @event hide-overlay - * @memberof PSV - * @summary Triggered when the overlay is hidden - * @param {string} [id] - */ - HIDE_OVERLAY : 'hide-overlay', - /** - * @event hide-tooltip - * @memberof PSV - * @summary Triggered when the tooltip is hidden - * @param {*} Data associated to this tooltip - */ - HIDE_TOOLTIP : 'hide-tooltip', - /** - * @event key-press - * @memberof PSV - * @summary Triggered when a key is pressed, can be cancelled - * @param {string} key - */ - KEY_PRESS : 'key-press', - /** - * @event load-progress - * @memberof PSV - * @summary Triggered when the loader value changes - * @param {number} value from 0 to 100 - */ - LOAD_PROGRESS : 'load-progress', - /** - * @event open-panel - * @memberof PSV - * @summary Triggered when the panel is opened - * @param {string} [id] - */ - OPEN_PANEL : 'open-panel', - /** - * @event panorama-loaded - * @memberof PSV - * @summary Triggered when a panorama image has been loaded - * @param {PSV.TextureData} textureData - */ - PANORAMA_LOADED : 'panorama-loaded', - /** - * @event position-updated - * @memberof PSV - * @summary Triggered when the view longitude and/or latitude changes - * @param {PSV.Position} position - */ - POSITION_UPDATED : 'position-updated', - /** - * @event ready - * @memberof PSV - * @summary Triggered when the panorama image has been loaded and the viewer is ready to perform the first render - */ - READY : 'ready', - /** - * @event render - * @memberof PSV - * @summary Triggered on each viewer render, **this event is triggered very often** - */ - RENDER : 'render', - /** - * @event show-notification - * @memberof PSV - * @summary Triggered when the notification is shown - * @param {string} [id] - */ - SHOW_NOTIFICATION : 'show-notification', - /** - * @event show-overlay - * @memberof PSV - * @summary Triggered when the overlay is shown - * @param {string} [id] - */ - SHOW_OVERLAY : 'show-overlay', - /** - * @event show-tooltip - * @memberof PSV - * @summary Triggered when the tooltip is shown - * @param {*} Data associated to this tooltip - * @param {PSV.components.Tooltip} Instance of the tooltip - */ - SHOW_TOOLTIP : 'show-tooltip', - /** - * @event size-updated - * @memberof PSV - * @summary Triggered when the viewer size changes - * @param {PSV.Size} size - */ - SIZE_UPDATED : 'size-updated', - /** - * @event stop-all - * @memberof PSV - * @summary Triggered when all current animations are stopped - */ - STOP_ALL : 'stop-all', - /** - * @event zoom-updated - * @memberof PSV - * @summary Triggered when the zoom level changes - * @param {number} zoomLevel - */ - ZOOM_UPDATED : 'zoom-updated', -}; - -/** - * @summary Available change events names - * @memberOf PSV.constants - * @enum {string} - * @constant - */ -export const CHANGE_EVENTS = { - /** - * @event get-animate-position - * @memberof PSV - * @param {Position} position - * @returns {Position} - * @summary Called to alter the target position of an animation - */ - GET_ANIMATE_POSITION: 'get-animate-position', - /** - * @event get-rotate-position - * @memberof PSV - * @param {Position} position - * @returns {Position} - * @summary Called to alter the target position of a rotation - */ - GET_ROTATE_POSITION : 'get-rotate-position', -}; - -/** - * @summary Special events emitted to listener using {@link Viewer#observeObjects} - * @memberOf PSV.constants - * @constant - * @package - */ -export const OBJECT_EVENTS = { - ENTER_OBJECT: 'enter-object', - HOVER_OBJECT: 'hover-object', - LEAVE_OBJECT: 'leave-object', -}; - -/** - * @summary Internal identifiers for various stuff - * @memberOf PSV.constants - * @enum {string} - * @constant - */ -export const IDS = { - MENU : 'menu', - TWO_FINGERS: 'twoFingers', - CTRL_ZOOM : 'ctrlZoom', - ERROR : 'error', - DESCRIPTION: 'description', -}; - -/* eslint-disable */ -// @formatter:off -/** - * @summary Collection of easing functions - * @memberOf PSV.constants - * @see {@link https://gist.github.com/frederickk/6165768} - * @type {Object} - * @constant - */ -export const EASINGS = { - linear : (t) => t, - - inQuad : (t) => t*t, - outQuad : (t) => t*(2-t), - inOutQuad : (t) => t<.5 ? 2*t*t : -1+(4-2*t)*t, - - inCubic : (t) => t*t*t, - outCubic : (t) => (--t)*t*t+1, - inOutCubic: (t) => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1, - - inQuart : (t) => t*t*t*t, - outQuart : (t) => 1-(--t)*t*t*t, - inOutQuart: (t) => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t, - - inQuint : (t) => t*t*t*t*t, - outQuint : (t) => 1+(--t)*t*t*t*t, - inOutQuint: (t) => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t, - - inSine : (t) => 1-Math.cos(t*(Math.PI/2)), - outSine : (t) => Math.sin(t*(Math.PI/2)), - inOutSine : (t) => .5-.5*Math.cos(Math.PI*t), - - inExpo : (t) => Math.pow(2, 10*(t-1)), - outExpo : (t) => 1-Math.pow(2, -10*t), - inOutExpo : (t) => (t=t*2-1)<0 ? .5*Math.pow(2, 10*t) : 1-.5*Math.pow(2, -10*t), - - inCirc : (t) => 1-Math.sqrt(1-t*t), - outCirc : (t) => Math.sqrt(1-(t-1)*(t-1)), - inOutCirc : (t) => (t*=2)<1 ? .5-.5*Math.sqrt(1-t*t) : .5+.5*Math.sqrt(1-(t-=2)*t) -}; -// @formatter:on -/* eslint-enable */ - -/** - * @summary Subset of key codes - * @memberOf PSV.constants - * @type {Object} - * @constant - */ -export const KEY_CODES = { - Enter : 'Enter', - Control : 'Control', - Escape : 'Escape', - Space : ' ', - PageUp : 'PageUp', - PageDown : 'PageDown', - ArrowLeft : 'ArrowLeft', - ArrowUp : 'ArrowUp', - ArrowRight: 'ArrowRight', - ArrowDown : 'ArrowDown', - Delete : 'Delete', - Plus : '+', - Minus : '-', -}; diff --git a/src/data/system.js b/src/data/system.js deleted file mode 100644 index eb021b306..000000000 --- a/src/data/system.js +++ /dev/null @@ -1,223 +0,0 @@ -import { PSVError } from '../PSVError'; -import { VIEWER_DATA } from './constants'; - -const LOCALSTORAGE_TOUCH_SUPPORT = `${VIEWER_DATA}_touchSupport`; - -/** - * @summary General information about the system - * @constant - * @memberOf PSV - * @property {boolean} loaded - Indicates if the system data has been loaded - * @property {Function} load - Loads the system if not already loaded - * @property {number} pixelRatio - * @property {boolean} isWebGLSupported - * @property {number} maxCanvasWidth - * @property {string} mouseWheelEvent - * @property {string} fullscreenEvent - * @property {Function} getMaxCanvasWidth - Returns the max width of a canvas allowed by the browser - * @property {{initial: boolean, promise: Promise}} isTouchEnabled - */ -export const SYSTEM = { - loaded : false, - pixelRatio : 1, - isWebGLSupported: false, - isTouchEnabled : null, - maxTextureWidth : 0, - mouseWheelEvent : null, - fullscreenEvent : null, -}; - -/** - * @summary Loads the system if not already loaded - */ -SYSTEM.load = () => { - if (!SYSTEM.loaded) { - const ctx = getWebGLCtx(); - - SYSTEM.loaded = true; - SYSTEM.pixelRatio = window.devicePixelRatio || 1; - SYSTEM.isWebGLSupported = ctx != null; - SYSTEM.isTouchEnabled = isTouchEnabled(); - SYSTEM.maxTextureWidth = getMaxTextureWidth(ctx); - SYSTEM.mouseWheelEvent = getMouseWheelEvent(); - SYSTEM.fullscreenEvent = getFullscreenEvent(); - } -}; - -let maxCanvasWidth = null; -SYSTEM.getMaxCanvasWidth = () => { - if (maxCanvasWidth === null) { - maxCanvasWidth = getMaxCanvasWidth(SYSTEM.maxTextureWidth); - } - return maxCanvasWidth; -}; - -/** - * @summary Tries to return a canvas webgl context - * @returns {WebGLRenderingContext} - * @private - */ -function getWebGLCtx() { - const canvas = document.createElement('canvas'); - const names = ['webgl', 'experimental-webgl', 'moz-webgl', 'webkit-3d']; - let context = null; - - if (!canvas.getContext) { - return null; - } - - if (names.some((name) => { - try { - context = canvas.getContext(name); - return context !== null; - } - catch (e) { - return false; - } - })) { - return context; - } - else { - return null; - } -} - -/** - * @summary Detects if the user is using a touch screen - * @returns {{initial: boolean, promise: Promise}} - * @private - */ -function isTouchEnabled() { - let initial = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); - if (LOCALSTORAGE_TOUCH_SUPPORT in localStorage) { - initial = localStorage[LOCALSTORAGE_TOUCH_SUPPORT] === 'true'; - } - - const promise = new Promise((resolve) => { - let clear; - - const listenerMouse = () => { - clear(); - localStorage[LOCALSTORAGE_TOUCH_SUPPORT] = false; - resolve(false); - }; - - const listenerTouch = () => { - clear(); - localStorage[LOCALSTORAGE_TOUCH_SUPPORT] = true; - resolve(true); - }; - - const listenerTimeout = () => { - clear(); - localStorage[LOCALSTORAGE_TOUCH_SUPPORT] = initial; - resolve(initial); - }; - - window.addEventListener('mousedown', listenerMouse, false); - window.addEventListener('touchstart', listenerTouch, false); - const listenerTimeoutId = setTimeout(listenerTimeout, 10000); - - clear = () => { - window.removeEventListener('mousedown', listenerMouse); - window.removeEventListener('touchstart', listenerTouch); - clearTimeout(listenerTimeoutId); - }; - }); - - return { initial, promise }; -} - -/** - * @summary Gets max texture width in WebGL context - * @returns {number} - * @private - */ -function getMaxTextureWidth(ctx) { - if (ctx !== null) { - return ctx.getParameter(ctx.MAX_TEXTURE_SIZE); - } - else { - return 0; - } -} - -/** - * @summary Gets max canvas width supported by the browser. - * We only test powers of 2 and height = width / 2 because that's what we need to generate WebGL textures - * @param maxWidth - * @return {number} - * @private - */ -function getMaxCanvasWidth(maxWidth) { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - canvas.width = maxWidth; - canvas.height = maxWidth / 2; - - while (canvas.width > 1024) { - ctx.fillStyle = 'white'; - ctx.fillRect(0, 0, 1, 1); - - try { - if (ctx.getImageData(0, 0, 1, 1).data[0] > 0) { - return canvas.width; - } - } - catch (e) { - // continue - } - - canvas.width /= 2; - canvas.height /= 2; - } - - throw new PSVError('Unable to detect system capabilities'); -} - -/** - * @summary Gets the event name for mouse wheel - * @returns {string} - * @private - */ -function getMouseWheelEvent() { - if ('onwheel' in document.createElement('div')) { // Modern browsers support "wheel" - return 'wheel'; - } - else if (document.onmousewheel !== undefined) { // Webkit and IE support at least "mousewheel" - return 'mousewheel'; - } - else { // let's assume that remaining browsers are older Firefox - return 'DOMMouseScroll'; - } -} - -/** - * @summary Map between fullsceen method and fullscreen event name - * @type {Object} - * @readonly - * @private - */ -const FULLSCREEN_EVT_MAP = { - exitFullscreen : 'fullscreenchange', - webkitExitFullscreen: 'webkitfullscreenchange', - mozCancelFullScreen : 'mozfullscreenchange', - msExitFullscreen : 'MSFullscreenChange', -}; - - -/** - * @summary Gets the event name for fullscreen - * @returns {string} - * @private - */ -function getFullscreenEvent() { - const validExits = Object.keys(FULLSCREEN_EVT_MAP).filter(exit => exit in document); - - if (validExits.length) { - return FULLSCREEN_EVT_MAP[validExits[0]]; - } - else { - return null; - } -} diff --git a/src/icons/play-active.svg b/src/icons/play-active.svg deleted file mode 100644 index 3b83aa24c..000000000 --- a/src/icons/play-active.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/icons/play.svg b/src/icons/play.svg deleted file mode 100644 index 9ae0f5613..000000000 --- a/src/icons/play.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/index.js b/src/index.js deleted file mode 100644 index b52283e65..000000000 --- a/src/index.js +++ /dev/null @@ -1,218 +0,0 @@ -import { AbstractAdapter } from './adapters/AbstractAdapter'; -import { EquirectangularAdapter } from './adapters/equirectangular'; -import { AbstractButton } from './buttons/AbstractButton'; -import { AbstractComponent } from './components/AbstractComponent'; -import { registerButton } from './components/Navbar'; -import { DEFAULTS } from './data/config'; -import * as CONSTANTS from './data/constants'; -import './data/constants'; // for jsdoc -import { SYSTEM } from './data/system'; -import { AbstractPlugin } from './plugins/AbstractPlugin'; -import { PSVError } from './PSVError'; -import * as utils from './utils'; -import { Viewer } from './Viewer'; -import './styles/index.scss'; - -export { - AbstractAdapter, - AbstractButton, - AbstractComponent, - AbstractPlugin, - CONSTANTS, - DEFAULTS, - EquirectangularAdapter, - PSVError, - registerButton, - SYSTEM, - utils, - Viewer -}; - - -/** - * @namespace PSV - */ - -/** - * @typedef {Object} PSV.Point - * @summary Object defining a point - * @property {number} x - * @property {number} y - */ - -/** - * @typedef {Object} PSV.Size - * @summary Object defining a size - * @property {number} width - * @property {number} height - */ - -/** - * @typedef {Object} PSV.CssSize - * @summary Object defining a size in CSS - * @property {string} [width] - * @property {string} [height] - */ - -/** - * @typedef {Object} PSV.SphereCorrection - * @summary Object defining angular corrections to a sphere - * @property {number} pan - * @property {number} tilt - * @property {number} roll - */ - -/** - * @typedef {Object} PSV.Position - * @summary Object defining a spherical position - * @property {number} longitude - * @property {number} latitude - */ - -/** - * @typedef {PSV.Position | PSV.Point} PSV.ExtendedPosition - * @summary Object defining a spherical or texture position - * @description A position that can be expressed either in spherical coordinates (radians or degrees) or in texture coordinates (pixels) - */ - -/** - * @typedef {PSV.ExtendedPosition} PSV.AnimateOptions - * @summary Object defining animation options - * @property {number|string} speed - animation speed or duration in milliseconds - * @property {number} [zoom] - new zoom level between 0 and 100 - */ - -/** - * @typedef {Object} PSV.PanoData - * @summary Crop information of the panorama - * @property {number} fullWidth - * @property {number} fullHeight - * @property {number} croppedWidth - * @property {number} croppedHeight - * @property {number} croppedX - * @property {number} croppedY - * @property {number} [poseHeading] - * @property {number} [posePitch] - * @property {number} [poseRoll] - */ - -/** - * @callback PanoDataProvider - * @summary Function to compute panorama data once the image is loaded - * @memberOf PSV - * @param {Image} image - loaded image - * @returns {PSV.PanoData} computed panorama data - */ - -/** - * @typedef {PSV.ExtendedPosition} PSV.PanoramaOptions - * @summary Object defining panorama and animation options - * @property {string} [caption] - new navbar caption - * @property {string} [description] - new panel description - * @property {boolean|number} [transition=1500] - duration of the transition between all and new panorama - * @property {boolean} [showLoader=true] - show the loader while loading the new panorama - * @property {number} [zoom] - new zoom level between 0 and 100 - * @property {PSV.SphereCorrection} [sphereCorrection] - new sphere correction to apply to the panorama - * @property {PSV.PanoData | PSV.PanoDataProvider} [panoData] - new data used for this panorama - * @property {*} [overlay] - new overlay to apply to the panorama - * @property {number} [overlayOpacity] - new overlay opacity - */ - -/** - * @typedef {Object} PSV.TextureData - * @summary Result of the {@link PSV.adapters.AbstractAdapter#loadTexture} method - * @property {*} panorama - * @property {external:THREE.Texture|external:THREE.Texture[]|Record} texture - * @property {PSV.PanoData} [panoData] - */ - -/** - * @typedef {Object} PSV.ClickData - * @summary Data of the `click` event - * @property {boolean} rightclick - if it's a right click - * @property {number} clientX - position in the browser window - * @property {number} clientY - position in the browser window - * @property {number} viewerX - position in the viewer - * @property {number} viewerY - position in the viewer - * @property {number} longitude - position in spherical coordinates - * @property {number} latitude - position in spherical coordinates - * @property {number} [textureX] - position on the texture, if applicable - * @property {number} [textureY] - position on the texture, if applicable - * @property {PSV.plugins.MarkersPlugin.Marker} [marker] - clicked marker - * @property {THREE.Object3D[]} [objects] - * @property {EventTarget} [target] - */ - -/** - * @typedef {Object} PSV.NavbarCustomButton - * @summary Definition of a custom navbar button - * @property {string} [id] - * @property {string} [title] - * @property {string} [content] - * @property {string} [className] - * @property {function} onClick - * @property {boolean} [disabled=false] - * @property {boolean} [visible=true] - * @property {boolean} [collapsable=true] - * @property {boolean} [tabbable=true] - */ - -/** - * @typedef {Object} PSV.Options - * @summary Viewer options, see {@link https://photo-sphere-viewer.js.org/guide/config.html} - */ - -/** - * @external THREE - * @description {@link https://threejs.org} - */ - -/** - * @typedef {Object} external:THREE.Vector3 - * @summary {@link https://threejs.org/docs/index.html#api/en/math/Vector3} - */ - -/** - * @typedef {Object} external:THREE.Euler - * @summary {@link https://threejs.org/docs/index.html#api/en/math/Euler} - */ - -/** - * @typedef {Object} external:THREE.Texture - * @summary {@link https://threejs.org/docs/index.html#api/en/textures/Texture} - */ - -/** - * @typedef {Object} external:THREE.Scene - * @summary {@link https://threejs.org/docs/index.html#api/en/scenes/Scene} - */ - -/** - * @typedef {Object} external:THREE.WebGLRenderer - * @summary {@link https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderer} - */ - -/** - * @typedef {Object} external:THREE.PerspectiveCamera - * @summary {@link https://threejs.org/docs/index.html#api/en/cameras/PerspectiveCamera} - */ - -/** - * @typedef {Object} external:THREE.Mesh - * @summary {@link https://threejs.org/docs/index.html#api/en/objects/Mesh} - */ - -/** - * @typedef {Object} external:THREE.Raycaster - * @summary {@link https://threejs.org/docs/index.html#api/en/core/Raycaster} - */ - -/** - * @external uEvent - * @description {@link https://github.com/mistic100/uEvent} - */ - -/** - * @typedef {Object} external:uEvent.EventEmitter - * @description {@link https://github.com/mistic100/uEvent#api} - */ diff --git a/src/plugins/AbstractPlugin.js b/src/plugins/AbstractPlugin.js deleted file mode 100644 index 278ab0c51..000000000 --- a/src/plugins/AbstractPlugin.js +++ /dev/null @@ -1,51 +0,0 @@ -import { EventEmitter } from 'uevent'; - -/** - * @namespace PSV.plugins - */ - -/** - * @summary Base plugins class - * @memberof PSV.plugins - * @abstract - */ -export class AbstractPlugin extends EventEmitter { - - /** - * @summary Unique identifier of the plugin - * @member {string} - * @readonly - * @static - */ - static id = null; - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(); - - /** - * @summary Reference to main controller - * @type {PSV.Viewer} - * @readonly - */ - this.psv = psv; - } - - /** - * @summary Initializes the plugin - * @package - */ - init() { - } - - /** - * @summary Destroys the plugin - * @package - */ - destroy() { - delete this.psv; - } - -} diff --git a/src/plugins/autorotate-keypoints/index.js b/src/plugins/autorotate-keypoints/index.js deleted file mode 100644 index 78211f5ec..000000000 --- a/src/plugins/autorotate-keypoints/index.js +++ /dev/null @@ -1,418 +0,0 @@ -import { SplineCurve, Vector2 } from 'three'; -import { AbstractPlugin, CONSTANTS, PSVError, utils } from '../..'; - -/** - * @typedef {Object} PSV.plugins.AutorotateKeypointsPlugin.KeypointObject - * @property {PSV.ExtendedPosition} [position] - * @property {string} [markerId] - use the position and tooltip of a marker - * @property {number} [pause=0] - pause the animation when reaching this point, will display the tooltip if available - * @property {string|{content: string, position: string}} [tooltip] - */ - -/** - * @typedef {PSV.ExtendedPosition|string|PSV.plugins.AutorotateKeypointsPlugin.KeypointObject} PSV.plugins.AutorotateKeypointsPlugin.Keypoint - * @summary Definition of keypoints for automatic rotation, can be a position object, a marker id or an keypoint object - */ - -/** - * @typedef {Object} PSV.plugins.AutorotateKeypointsPlugin.Options - * @property {boolean} [startFromClosest=true] - start from the closest keypoint instead of the first keypoint - * @property {PSV.plugins.AutorotateKeypointsPlugin.Keypoint[]} keypoints - */ - - -const NUM_STEPS = 16; - -function serializePt(position) { - return [position.longitude, position.latitude]; -} - - -/** - * @summary Replaces the standard autorotate animation by a smooth transition between multiple points - * @extends PSV.plugins.AbstractPlugin - * @memberof PSV.plugins - */ -export class AutorotateKeypointsPlugin extends AbstractPlugin { - - static id = 'autorotate-keypoints'; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.plugins.AutorotateKeypointsPlugin.Options} [options] - */ - constructor(psv, options) { - super(psv); - - /** - * @member {Object} - * @property {number} idx - current index in keypoints - * @property {number[][]} curve - curve between idx and idx + 1 - * @property {number[]} startStep - start point of the current step - * @property {number[]} endStep - end point of the current step - * @property {number} startTime - start time of the current step - * @property {number} stepDuration - expected duration of the step - * @property {number} remainingPause - time remaining for the pause - * @property {number} lastTime - previous timestamp in render loop - * @property {PSV.components.Tooltip} tooltip - currently displayed tooltip - * @private - */ - this.state = {}; - - /** - * @member {PSV.plugins.AutorotateKeypointsPlugin.Options} - * @private - */ - this.config = { - startFromClosest: true, - ...options, - }; - - /** - * @type {PSV.plugins.AutorotateKeypointsPlugin.Keypoint[]} keypoints - */ - this.keypoints = null; - - /** - * @type {PSV.plugins.MarkersPlugin} - * @private - */ - this.markers = null; - } - - /** - * @package - */ - init() { - super.init(); - - this.markers = this.psv.getPlugin('markers'); - - if (this.config.keypoints) { - this.setKeypoints(this.config.keypoints); - delete this.config.keypoints; - } - - this.psv.on(CONSTANTS.EVENTS.AUTOROTATE, this); - this.psv.on(CONSTANTS.EVENTS.BEFORE_RENDER, this); - } - - /** - * @package - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.AUTOROTATE, this); - this.psv.off(CONSTANTS.EVENTS.BEFORE_RENDER, this); - - delete this.markers; - delete this.keypoints; - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - if (e.type === CONSTANTS.EVENTS.AUTOROTATE) { - this.__configure(); - } - else if (e.type === CONSTANTS.EVENTS.BEFORE_RENDER) { - this.__beforeRender(e.args[0]); - } - } - - /** - * @summary Changes the keypoints - * @param {PSV.plugins.AutorotateKeypointsPlugin.Keypoint[]} keypoints - */ - setKeypoints(keypoints) { - if (keypoints?.length < 2) { - throw new PSVError('At least two points are required'); - } - - this.keypoints = utils.clone(keypoints); - - if (this.keypoints) { - this.keypoints.forEach((pt, i) => { - if (typeof pt === 'string') { - pt = { markerId: pt }; - } - else if (utils.isExtendedPosition(pt)) { - pt = { position: pt }; - } - if (pt.markerId) { - if (!this.markers) { - throw new PSVError(`Keypoint #${i} references a marker but the markers plugin is not loaded`); - } - const marker = this.markers.getMarker(pt.markerId); - pt.position = serializePt(marker.props.position); - } - else if (pt.position) { - pt.position = serializePt(this.psv.dataHelper.cleanPosition(pt.position)); - } - else { - throw new PSVError(`Keypoint #${i} is missing marker or position`); - } - - if (typeof pt.tooltip === 'string') { - pt.tooltip = { content: pt.tooltip }; - } - - this.keypoints[i] = pt; - }); - } - - this.__configure(); - } - - /** - * @private - */ - __configure() { - if (!this.psv.isAutorotateEnabled() || !this.keypoints) { - this.__hideTooltip(); - this.state = {}; - return; - } - - // cancel core rotation - this.psv.dynamics.position.stop(); - - this.state = { - idx : -1, - curve : [], - startStep : null, - endStep : null, - startTime : null, - stepDuration : null, - remainingPause: null, - lastTime : null, - tooltip : null, - }; - - if (this.config.startFromClosest) { - const currentPosition = serializePt(this.psv.getPosition()); - const index = this.__findMinIndex(this.keypoints, (keypoint) => { - return utils.greatArcDistance(keypoint.position, currentPosition); - }); - - this.keypoints.push(...this.keypoints.splice(0, index)); - } - } - - /** - * @private - */ - __beforeRender(timestamp) { - if (this.psv.isAutorotateEnabled()) { - // initialisation - if (!this.state.startTime) { - this.state.endStep = serializePt(this.psv.getPosition()); - this.__nextStep(); - - this.state.startTime = timestamp; - this.state.lastTime = timestamp; - } - - this.__nextFrame(timestamp); - } - } - - /** - * @private - */ - __incrementIdx() { - this.state.idx++; - if (this.state.idx === this.keypoints.length) { - this.state.idx = 0; - } - } - - /** - * @private - */ - __showTooltip() { - const keypoint = this.keypoints[this.state.idx]; - - if (keypoint.tooltip) { - const position = this.psv.dataHelper.vector3ToViewerCoords(this.psv.prop.direction); - - this.state.tooltip = this.psv.tooltip.create({ - content : keypoint.tooltip.content, - position: keypoint.tooltip.position, - top : position.y, - left : position.x, - }); - } - else if (keypoint.markerId) { - const marker = this.markers.getMarker(keypoint.markerId); - marker.showTooltip(); - this.state.tooltip = marker.tooltip; - } - } - - /** - * @private - */ - __hideTooltip() { - if (this.state.tooltip) { - const keypoint = this.keypoints[this.state.idx]; - - if (keypoint.tooltip) { - this.state.tooltip.hide(); - } - else if (keypoint.markerId) { - const marker = this.markers.getMarker(keypoint.markerId); - marker.hideTooltip(); - } - - this.state.tooltip = null; - } - } - - /** - * @private - */ - __nextPoint() { - // get the 4 points necessary to compute the current movement - // the two points of the current segments and one point before and after - const workPoints = []; - if (this.state.idx === -1) { - const currentPosition = serializePt(this.psv.getPosition()); - workPoints.push( - currentPosition, - currentPosition, - this.keypoints[0].position, - this.keypoints[1].position - ); - } - else { - for (let i = -1; i < 3; i++) { - const keypoint = this.state.idx + i < 0 - ? this.keypoints[this.keypoints.length - 1] - : this.keypoints[(this.state.idx + i) % this.keypoints.length]; - workPoints.push(keypoint.position); - } - } - - // apply offsets to avoid crossing the origin - const workVectors = [new Vector2(workPoints[0][0], workPoints[0][1])]; - - let k = 0; - for (let i = 1; i <= 3; i++) { - const d = workPoints[i - 1][0] - workPoints[i][0]; - if (d > Math.PI) { // crossed the origin left to right - k += 1; - } - else if (d < -Math.PI) { // crossed the origin right to left - k -= 1; - } - if (k !== 0 && i === 1) { - // do not modify first point, apply the reverse offset the the previous point instead - workVectors[0].x -= k * 2 * Math.PI; - k = 0; - } - workVectors.push(new Vector2(workPoints[i][0] + k * 2 * Math.PI, workPoints[i][1])); - } - - const curve = new SplineCurve(workVectors) - .getPoints(NUM_STEPS * 3) - .map(p => ([p.x, p.y])); - - // debugCurve(this.markers, curve, NUM_STEPS); - - // only keep the curve for the current movement - this.state.curve = curve.slice(NUM_STEPS + 1, NUM_STEPS * 2 + 1); - - if (this.state.idx !== -1) { - this.state.remainingPause = this.keypoints[this.state.idx].pause; - - if (this.state.remainingPause) { - this.__showTooltip(); - } - else { - this.__incrementIdx(); - } - } - else { - this.__incrementIdx(); - } - } - - /** - * @private - */ - __nextStep() { - if (this.state.curve.length === 0) { - this.__nextPoint(); - - // reset transformation made to the previous point - this.state.endStep[0] = utils.parseAngle(this.state.endStep[0]); - } - - // target next point - this.state.startStep = this.state.endStep; - this.state.endStep = this.state.curve.shift(); - - // compute duration from distance and speed - const distance = utils.greatArcDistance(this.state.startStep, this.state.endStep); - this.state.stepDuration = distance * 1000 / Math.abs(this.psv.config.autorotateSpeed); - - if (distance === 0) { // edge case - this.__nextStep(); - } - } - - /** - * @private - */ - __nextFrame(timestamp) { - const ellapsed = timestamp - this.state.lastTime; - this.state.lastTime = timestamp; - - // currently paused - if (this.state.remainingPause) { - this.state.remainingPause = Math.max(0, this.state.remainingPause - ellapsed); - if (this.state.remainingPause > 0) { - return; - } - else { - this.__hideTooltip(); - this.__incrementIdx(); - this.state.startTime = timestamp; - } - } - - let progress = (timestamp - this.state.startTime) / this.state.stepDuration; - if (progress >= 1) { - this.__nextStep(); - progress = 0; - this.state.startTime = timestamp; - } - - this.psv.rotate({ - longitude: this.state.startStep[0] + (this.state.endStep[0] - this.state.startStep[0]) * progress, - latitude : this.state.startStep[1] + (this.state.endStep[1] - this.state.startStep[1]) * progress, - }); - } - - /** - * @private - */ - __findMinIndex(array, mapper) { - let idx = 0; - let current = Number.MAX_VALUE; - - array.forEach((item, i) => { - const value = mapper ? mapper(item) : item; - if (value < current) { - current = value; - idx = i; - } - }); - - return idx; - } - -} diff --git a/src/plugins/compass/index.js b/src/plugins/compass/index.js deleted file mode 100644 index dab4911aa..000000000 --- a/src/plugins/compass/index.js +++ /dev/null @@ -1,386 +0,0 @@ -import { MathUtils } from 'three'; -import { AbstractPlugin, CONSTANTS, SYSTEM, utils } from '../..'; -import compass from './compass.svg'; -import './style.scss'; - - -/** - * @typedef {Object} PSV.plugins.CompassPlugin.Options - * @property {string} [size='120px'] - size of the compass - * @property {string} [position='top left'] - position of the compass - * @property {string} [backgroundSvg] - SVG used as background of the compass - * @property {string} [coneColor='rgba(255, 255, 255, 0.5)'] - color of the cone of the compass - * @property {boolean} [navigation=true] - allows to click on the compass to rotate the viewer - * @property {string} [navigationColor='rgba(255, 0, 0, 0.2)'] - color of the navigation cone - * @property {PSV.plugins.CompassPlugin.Hotspot[]} [hotspots] - small dots visible on the compass (will contain every marker with the "compass" data) - * @property {string} [hotspotColor='rgba(0, 0, 0, 0.5)'] - default color of hotspots - */ - -/** - * @typedef {PSV.ExtendedPosition} PSV.plugins.CompassPlugin.Hotspot - * @type {string} [color] - override the global "hotspotColor" - */ - - -const HOTSPOT_SIZE_RATIO = 1 / 40; - - -/** - * @summary Adds a compass on the viewer - * @extends PSV.plugins.AbstractPlugin - * @memberof PSV.plugins - */ -export class CompassPlugin extends AbstractPlugin { - - static id = 'compass'; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.plugins.CompassPlugin.Options} options - */ - constructor(psv, options) { - super(psv); - - /** - * @member {PSV.plugins.CompassPlugin.Options} - * @private - */ - this.config = { - size : '120px', - backgroundSvg : compass, - coneColor : 'rgba(255, 255, 255, 0.5)', - navigation : true, - navigationColor: 'rgba(255, 0, 0, 0.2)', - hotspotColor : 'rgba(0, 0, 0, 0.5)', - ...options, - position : utils.cleanPosition(options.position, { allowCenter: true, cssOrder: true }) || ['top', 'left'], - }; - - /** - * @private - */ - this.prop = { - visible : true, - mouse : null, - mouseDown: false, - markers : [], - }; - - /** - * @type {PSV.plugins.MarkersPlugin} - * @private - */ - this.markers = null; - - /** - * @member {HTMLElement} - * @readonly - * @private - */ - this.container = document.createElement('div'); - this.container.className = `psv-compass psv-compass--${this.config.position.join('-')}`; - this.container.innerHTML = this.config.backgroundSvg; - - this.container.style.width = this.config.size; - this.container.style.height = this.config.size; - if (this.config.position[0] === 'center') { - this.container.style.marginTop = `calc(-${this.config.size} / 2)`; - } - if (this.config.position[1] === 'center') { - this.container.style.marginLeft = `calc(-${this.config.size} / 2)`; - } - - /** - * @member {HTMLCanvasElement} - * @readonly - * @private - */ - this.canvas = document.createElement('canvas'); - - this.container.appendChild(this.canvas); - - if (this.config.navigation) { - this.container.addEventListener('mouseenter', this); - this.container.addEventListener('mouseleave', this); - this.container.addEventListener('mousemove', this); - this.container.addEventListener('mousedown', this); - this.container.addEventListener('mouseup', this); - this.container.addEventListener('touchstart', this); - this.container.addEventListener('touchmove', this); - this.container.addEventListener('touchend', this); - } - } - - /** - * @package - */ - init() { - super.init(); - - this.markers = this.psv.getPlugin('markers'); - - this.psv.container.appendChild(this.container); - - this.canvas.width = this.container.clientWidth * SYSTEM.pixelRatio; - this.canvas.height = this.container.clientWidth * SYSTEM.pixelRatio; - - this.psv.on(CONSTANTS.EVENTS.RENDER, this); - - if (this.markers) { - this.markers.on('set-markers', this); - } - } - - /** - * @package - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.RENDER, this); - - if (this.markers) { - this.markers.off('set-markers', this); - } - - this.psv.container.removeChild(this.container); - - delete this.canvas; - delete this.container; - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - switch (e.type) { - case CONSTANTS.EVENTS.RENDER: - this.__update(); - break; - case 'set-markers': - this.prop.markers = e.args[0].filter(m => m.data?.compass); - this.__update(); - break; - case 'mouseenter': - case 'mousemove': - case 'touchmove': - this.prop.mouse = e.changedTouches?.[0] || e; - if (this.prop.mouseDown) { - this.__click(); - } - else { - this.__update(); - } - e.stopPropagation(); - e.preventDefault(); - break; - case 'mousedown': - case 'touchstart': - this.prop.mouseDown = true; - e.stopPropagation(); - e.preventDefault(); - break; - case 'mouseup': - case 'touchend': - this.prop.mouse = e.changedTouches?.[0] || e; - this.prop.mouseDown = false; - this.__click(); - if (e.changedTouches) { - this.prop.mouse = null; - this.__update(); - } - e.stopPropagation(); - e.preventDefault(); - break; - case 'mouseleave': - this.prop.mouse = null; - this.prop.mouseDown = false; - this.__update(); - break; - default: - break; - } - } - - /** - * @summary Hides the compass - */ - hide() { - this.container.style.display = 'none'; - this.prop.visible = false; - } - - /** - * @summary Shows the compass - */ - show() { - this.container.style.display = ''; - this.prop.visible = true; - } - - /** - * @summary Changes the hotspots on the compass - * @param {PSV.plugins.CompassPlugin.Hotspot[]} hotspots - */ - setHotspots(hotspots) { - this.config.hotspots = hotspots; - this.__update(); - } - - /** - * @summary Removes all hotspots - */ - clearHotspots() { - this.setHotspots(null); - } - - /** - * @summary Updates the compass for current zoom and position - * @private - */ - __update() { - const context = this.canvas.getContext('2d'); - context.clearRect(0, 0, this.canvas.width, this.canvas.height); - - const longitude = this.psv.getPosition().longitude; - const fov = MathUtils.degToRad(this.psv.prop.hFov); - - this.__drawCone(context, this.config.coneColor, longitude, fov); - - const mouseAngle = this.__getMouseAngle(); - if (mouseAngle !== null) { - this.__drawCone(context, this.config.navigationColor, mouseAngle, fov); - } - - this.prop.markers.forEach((marker) => { - this.__drawMarker(context, marker); - }); - this.config.hotspots?.forEach((spot) => { - if ('longitude' in spot && !('latitude' in spot)) { - spot.latitude = 0; - } - const pos = this.psv.dataHelper.cleanPosition(spot); - this.__drawPoint(context, spot.color || this.config.hotspotColor, pos.longitude, pos.latitude); - }); - } - - /** - * @summary Rotates the viewer depending on the position of the mouse on the compass - * @private - */ - __click() { - const mouseAngle = this.__getMouseAngle(); - - if (mouseAngle !== null) { - this.psv.rotate({ - longitude: mouseAngle, - latitude : 0, - }); - } - } - - /** - * @summary Draw a cone - * @param {CanvasRenderingContext2D} context - * @param {string} color - * @param {number} longitude - in viewer reference - * @param {number} fov - * @private - */ - __drawCone(context, color, longitude, fov) { - const a1 = longitude - Math.PI / 2 - fov / 2; - const a2 = a1 + fov; - const c = this.canvas.width / 2; - - context.beginPath(); - context.moveTo(c, c); - context.lineTo(c + Math.cos(a1) * c, c + Math.sin(a1) * c); - context.arc(c, c, c, a1, a2, false); - context.lineTo(c, c); - context.fillStyle = color; - context.fill(); - } - - /** - * @summary Draw a Marker - * @param {CanvasRenderingContext2D} context - * @param {PSV.plugins.MarkersPlugin.Marker} marker - * @private - */ - __drawMarker(context, marker) { - let color = this.config.hotspotColor; - if (typeof marker.data.compass === 'string') { - color = marker.data.compass; - } - - if (marker.isPoly()) { - context.beginPath(); - marker.props.def.forEach(([longitude, latitude], i) => { - const a = longitude - Math.PI / 2; - const d = (latitude + Math.PI / 2) / Math.PI; - const c = this.canvas.width / 2; - - context[i === 0 ? 'moveTo' : 'lineTo'](c + Math.cos(a) * c * d, c + Math.sin(a) * c * d); - }); - if (marker.isPolygon()) { - context.fillStyle = color; - context.fill(); - } - else { - context.strokeStyle = color; - context.lineWidth = Math.max(1, this.canvas.width * HOTSPOT_SIZE_RATIO / 2); - context.stroke(); - } - } - else { - const pos = marker.props.position; - this.__drawPoint(context, color, pos.longitude, pos.latitude); - } - } - - /** - * @summary Draw a point - * @param {CanvasRenderingContext2D} context - * @param {string} color - * @param {number} longitude - in viewer reference - * @param {number} latitude - in viewer reference - * @private - */ - __drawPoint(context, color, longitude, latitude) { - const a = longitude - Math.PI / 2; - const d = (latitude + Math.PI / 2) / Math.PI; - const c = this.canvas.width / 2; - const r = Math.max(2, this.canvas.width * HOTSPOT_SIZE_RATIO); - - context.beginPath(); - context.ellipse( - c + Math.cos(a) * c * d, c + Math.sin(a) * c * d, - r, r, - 0, 0, Math.PI * 2 - ); - context.fillStyle = color; - context.fill(); - } - - /** - * @summary Gets the longitude corresponding to the mouse position on the compass - * @return {number | null} - * @private - */ - __getMouseAngle() { - if (!this.prop.mouse) { - return null; - } - - const boundingRect = this.container.getBoundingClientRect(); - const mouseX = this.prop.mouse.clientX - boundingRect.left - boundingRect.width / 2; - const mouseY = this.prop.mouse.clientY - boundingRect.top - boundingRect.width / 2; - - if (Math.sqrt(mouseX * mouseX + mouseY * mouseY) > boundingRect.width / 2) { - return null; - } - - return Math.atan2(mouseY, mouseX) + Math.PI / 2; - } - -} diff --git a/src/plugins/compass/style.scss b/src/plugins/compass/style.scss deleted file mode 100644 index 83b0a79cd..000000000 --- a/src/plugins/compass/style.scss +++ /dev/null @@ -1,58 +0,0 @@ -@import '../../styles/vars'; - -$psv-compass-margin: 10px !default; - -.psv-compass { - position: absolute; - margin: $psv-compass-margin; - z-index: $psv-compass-zindex; - - @at-root .psv--has-navbar & { - margin-bottom: calc(#{$psv-navbar-height} + #{$psv-compass-margin}); - } - - canvas, - svg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - - &--top-left, - &--top-center, - &--top-right { - top: 0; - } - - &--center-left, - &--center-center, - &--center-right { - top: 50%; - } - - &--bottom-left, - &--bottom-center, - &--bottom-right { - bottom: 0; - } - - &--top-left, - &--center-left, - &--bottom-left { - left: 0; - } - - &--top-center, - &--center-center, - &--bottom-center { - left: 50%; - } - - &--top-right, - &--right-right, - &--bottom-right { - right: 0; - } -} diff --git a/src/plugins/gallery/GalleryButton.js b/src/plugins/gallery/GalleryButton.js deleted file mode 100644 index b5d5630b7..000000000 --- a/src/plugins/gallery/GalleryButton.js +++ /dev/null @@ -1,81 +0,0 @@ -import { AbstractButton } from '../..'; -import { EVENTS } from './constants'; -import gallery from './gallery.svg'; - -/** - * @summary Navigation bar gallery button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class GalleryButton extends AbstractButton { - - static id = 'gallery'; - static icon = gallery; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-gallery-button', true); - - /** - * @type {PSV.plugins.GalleryPlugin} - * @readonly - * @private - */ - this.plugin = this.psv.getPlugin('gallery'); - - if (this.plugin) { - this.plugin.on(EVENTS.SHOW_GALLERY, this); - this.plugin.on(EVENTS.HIDE_GALLERY, this); - } - - if (!this.plugin?.items.length) { - this.hide(); - } - } - - /** - * @override - */ - destroy() { - if (this.plugin) { - this.plugin.off(EVENTS.SHOW_GALLERY, this); - this.plugin.off(EVENTS.HIDE_GALLERY, this); - } - - delete this.plugin; - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - if (e.type === EVENTS.SHOW_GALLERY) { - this.toggleActive(true); - } - else if (e.type === EVENTS.HIDE_GALLERY) { - this.toggleActive(false); - } - } - - /** - * @override - */ - isSupported() { - return !!this.plugin; - } - - /** - * @override - * @description Toggles gallery - */ - onClick() { - this.plugin.toggle(); - } - -} diff --git a/src/plugins/gallery/GalleryComponent.js b/src/plugins/gallery/GalleryComponent.js deleted file mode 100644 index 884fb8a5d..000000000 --- a/src/plugins/gallery/GalleryComponent.js +++ /dev/null @@ -1,181 +0,0 @@ -import { AbstractComponent, SYSTEM, utils } from '../..'; -import blankIcon from './blank.svg'; -import { GALLERY_ITEM_DATA, GALLERY_ITEM_DATA_KEY, ITEMS_TEMPLATE } from './constants'; - -const ACTIVE_CLASS = 'psv-gallery-item--active'; - -/** - * @private - */ -export class GalleryComponent extends AbstractComponent { - - constructor(plugin) { - super(plugin.psv, 'psv-gallery psv--capture-event'); - - /** - * @type {SVGElement} - * @private - * @readonly - */ - this.blankIcon = (() => { - const temp = document.createElement('div'); - temp.innerHTML = blankIcon; - return temp.firstChild; - })(); - this.blankIcon.style.display = 'none'; - this.psv.container.appendChild(this.blankIcon); - - if ('IntersectionObserver' in window) { - /** - * @type {IntersectionObserver} - * @private - * @readonly - */ - this.observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.intersectionRatio > 0) { - entry.target.style.backgroundImage = `url(${entry.target.dataset.src})`; - delete entry.target.dataset.src; - this.observer.unobserve(entry.target); - } - }); - }, { - root: this.psv.container, - }); - } - - /** - * @type {PSV.plugins.GalleryPlugin} - * @private - * @readonly - */ - this.plugin = plugin; - - /** - * @type {Object} - * @private - */ - this.prop = { - ...this.prop, - mousedown : false, - initMouseX: null, - mouseX : null, - }; - - this.container.addEventListener(SYSTEM.mouseWheelEvent, this); - this.container.addEventListener('mousedown', this); - this.container.addEventListener('mousemove', this); - this.container.addEventListener('click', this); - window.addEventListener('mouseup', this); - - this.hide(); - } - - /** - * @override - */ - destroy() { - this.psv.container.removeChild(this.blankIcon); - - window.removeEventListener('mouseup', this); - - this.observer?.disconnect(); - - delete this.plugin; - delete this.blankIcon; - delete this.observer; - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case SYSTEM.mouseWheelEvent: - const spinY = utils.normalizeWheel(e).spinY; - this.container.scrollLeft += spinY * 50; - e.preventDefault(); - break; - - case 'mousedown': - this.prop.mousedown = true; - this.prop.initMouseX = e.clientX; - this.prop.mouseX = e.clientX; - break; - - case 'mousemove': - if (this.prop.mousedown) { - const delta = this.prop.mouseX - e.clientX; - this.container.scrollLeft += delta; - this.prop.mouseX = e.clientX; - } - break; - - case 'mouseup': - this.prop.mousedown = false; - this.prop.mouseX = null; - e.preventDefault(); - break; - - case 'click': - // prevent click on drag - if (Math.abs(this.prop.initMouseX - e.clientX) < 10) { - const item = utils.getClosest(e.target, `[data-${GALLERY_ITEM_DATA_KEY}]`); - if (item) { - this.plugin.__click(item.dataset[GALLERY_ITEM_DATA]); - } - } - break; - } - /* eslint-enable */ - } - - /** - * @override - */ - show() { - this.container.classList.add('psv-gallery--open'); - this.prop.visible = true; - } - - /** - * @override - */ - hide() { - this.container.classList.remove('psv-gallery--open'); - this.prop.visible = false; - } - - /** - * @summary Sets the list of items - * @param {PSV.plugins.GalleryPlugin.Item[]} items - */ - setItems(items) { - this.container.innerHTML = ITEMS_TEMPLATE(items, this.plugin.config.thumbnailSize); - - if (this.observer) { - this.observer.disconnect(); - - for (const child of this.container.querySelectorAll('[data-src]')) { - this.observer.observe(child); - } - } - } - - /** - * @param {number | string} id - */ - setActive(id) { - const currentActive = this.container.querySelector('.' + ACTIVE_CLASS); - currentActive?.classList.remove(ACTIVE_CLASS); - - if (id) { - const nextActive = this.container.querySelector(`[data-${GALLERY_ITEM_DATA_KEY}="${id}"]`); - nextActive?.classList.add(ACTIVE_CLASS); - } - } - -} diff --git a/src/plugins/gallery/constants.js b/src/plugins/gallery/constants.js deleted file mode 100644 index 9c9757ae2..000000000 --- a/src/plugins/gallery/constants.js +++ /dev/null @@ -1,60 +0,0 @@ -import { utils } from '../..'; - -/** - * @summary Available events - * @enum {string} - * @memberof PSV.plugins.GalleryPlugin - * @constant - */ -export const EVENTS = { - /** - * @event show-gallery - * @memberof PSV.plugins.GalleryPlugin - * @summary Triggered when the gallery is shown - */ - SHOW_GALLERY: 'show-gallery', - /** - * @event hide-gallery - * @memberof PSV.plugins.GalleryPlugin - * @summary Triggered when the gallery is hidden - */ - HIDE_GALLERY: 'hide-gallery', -}; - -/** - * @summary Property name added to gallery items - * @type {string} - * @constant - * @private - */ -export const GALLERY_ITEM_DATA = 'psvGalleryItem'; - -/** - * @summary Property name added to gallery items (dash-case) - * @type {string} - * @constant - * @private - */ -export const GALLERY_ITEM_DATA_KEY = utils.dasherize(GALLERY_ITEM_DATA); - -/** - * @summary Gallery template - * @param {PSV.plugins.GalleryPlugin.Item[]} items - * @param {PSV.Size} size - * @returns {string} - * @constant - * @private - */ -export const ITEMS_TEMPLATE = (items, size) => ` - -`; diff --git a/src/plugins/gallery/index.js b/src/plugins/gallery/index.js deleted file mode 100644 index f47f8fa2a..000000000 --- a/src/plugins/gallery/index.js +++ /dev/null @@ -1,222 +0,0 @@ -import { AbstractPlugin, CONSTANTS, DEFAULTS, PSVError, registerButton, utils } from '../..'; -import { EVENTS } from './constants'; -import { GalleryButton } from './GalleryButton'; -import { GalleryComponent } from './GalleryComponent'; -import './style.scss'; - -/** - * @typedef {Object} PSV.plugins.GalleryPlugin.Item - * @property {number|string} id - Unique identifier of the item - * @property {*} panorama - * @property {string} [thumbnail] - URL of the thumbnail - * @property {string} [name] - Text visible over the thumbnail - * @property {PSV.PanoramaOptions} [options] - Any option supported by the `setPanorama()` method - */ - -/** - * @typedef {Object} PSV.plugins.GalleryPlugin.Options - * @property {PSV.plugins.GalleryPlugin.Item[]} [items] - * @property {boolean} [visibleOnLoad=false] - Displays the gallery when loading the first panorama - * @property {boolean} [hideOnClick=true] - Hides the gallery when the user clicks on an item - * @property {PSV.Size} [thumbnailSize] - Size of thumbnails, default (200x100) is set with CSS - */ - - -// add gallery button -DEFAULTS.lang[GalleryButton.id] = 'Gallery'; -registerButton(GalleryButton, 'caption:left'); - - -export { EVENTS } from './constants'; - - -/** - * @summary Adds a gallery of multiple panoramas - * @extends PSV.plugins.AbstractPlugin - * @memberof PSV.plugins - */ -export class GalleryPlugin extends AbstractPlugin { - - static id = 'gallery'; - - static EVENTS = EVENTS; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.plugins.GalleryPlugin.Options} options - */ - constructor(psv, options) { - super(psv); - - /** - * @member {PSV.plugins.GalleryPlugin.Options} - * @private - */ - this.config = { - items : null, - visibleOnLoad: false, - hideOnClick : true, - thumbnailSize: { - width : 200, - height: 100, - }, - ...options, - }; - - /** - * @type {Object} - * @private - */ - this.prop = { - handler : null, - currentId: null, - }; - - /** - * @type {GalleryComponent} - * @private - * @readonly - */ - this.gallery = new GalleryComponent(this); - - /** - * @type {PSV.plugins.GalleryPlugin.Item[]} - * @private - */ - this.items = []; - } - - /** - * @package - */ - init() { - super.init(); - - this.psv.on(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - - if (this.config.visibleOnLoad) { - this.psv.once(CONSTANTS.EVENTS.READY, () => { - this.show(); - }); - } - - this.setItems(this.config.items); - delete this.config.items; - } - - /** - * @package - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - - this.gallery.destroy(); - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case CONSTANTS.EVENTS.PANORAMA_LOADED: - const item = this.items.find(i => utils.deepEqual(i.panorama, e.args[0].panorama)); - this.prop.currentId = item?.id; - this.gallery.setActive(item?.id); - break; - } - /* eslint-enable */ - } - - /** - * @summary Shows the gallery - * @fires PSV.plugins.GalleryPlugin.show-gallery - */ - show() { - this.trigger(EVENTS.SHOW_GALLERY); - return this.gallery.show(); - } - - /** - * @summary Hides the carousem - * @fires PSV.plugins.GalleryPlugin.hide-gallery - */ - hide() { - this.trigger(EVENTS.HIDE_GALLERY); - return this.gallery.hide(); - } - - /** - * @summary Hides or shows the gallery - */ - toggle() { - if (this.gallery.isVisible()) { - this.hide(); - } - else { - this.show(); - } - } - - /** - * @summary Sets the list of items - * @param {PSV.plugins.GalleryPlugin.Item[]} items - * @param {function} [handler] function that will be called when an item is clicked instead of the default behavior - */ - setItems(items, handler) { - if (!items?.length) { - items = []; - } - else { - items.forEach((item, i) => { - if (!item.id) { - throw new PSVError(`Item ${i} has no "id".`); - } - if (!item.panorama) { - throw new PSVError(`Item ${item.id} has no "panorama".`); - } - }); - } - - this.prop.handler = handler; - this.items = items.map(item => ({ - ...item, - id: `${item.id}`, - })); - - this.gallery.setItems(this.items); - - this.psv.navbar.getButton(GalleryButton.id, false)?.toggle(this.items.length > 0); - } - - /** - * @param {string} id - * @package - */ - __click(id) { - if (id === this.prop.currentId) { - return; - } - - if (this.prop.handler) { - this.prop.handler(id); - } - else { - const item = this.items.find(i => i.id === id); - this.psv.setPanorama(item.panorama, { - caption: item.name, - ...item.options, - }); - } - - this.prop.currentId = id; - this.gallery.setActive(id); - - if (this.config.hideOnClick) { - this.hide(); - } - } - -} diff --git a/src/plugins/gallery/style.scss b/src/plugins/gallery/style.scss deleted file mode 100644 index 5eb15975a..000000000 --- a/src/plugins/gallery/style.scss +++ /dev/null @@ -1,132 +0,0 @@ -@import '../../styles/vars'; - -$psv-gallery-padding: 15px !default; -$psv-gallery-border: 1px solid $psv-caption-color !default; -$psv-gallery-background: $psv-navbar-background !default; -$psv-gallery-item-radius: 5px !default; -$psv-gallery-item-active-border: 3px solid white !default; -$psv-gallery-title-font: $psv-caption-font !default; -$psv-gallery-title-color: $psv-caption-color !default; -$psv-gallery-title-background: rgba(0, 0, 0, .6) !default; -$psv-gallery-thumb-hover-scale: 1.2 !default; - -.psv-gallery { - position: absolute; - left: 0; - bottom: 0; - width: 100%; - background: $psv-gallery-background; - border-bottom: $psv-gallery-border; - overflow-x: auto; - overflow-y: hidden; - transition: transform ease-in-out .1s; - transform: translateY(100%); - z-index: $psv-navbar-zindex; - - @at-root .psv--has-navbar & { - bottom: $psv-navbar-height; - transform: translateY(calc(100% + #{$psv-navbar-height})); - } - - &--open { - transform: translateY(0) !important; - } - - &-container { - display: flex; - padding: $psv-gallery-padding; - } - - &-item { - flex: none; - position: relative; - margin-right: $psv-gallery-padding; - border-radius: $psv-gallery-item-radius; - overflow: hidden; - cursor: pointer; - - &-wrapper { - width: 100%; - height: 0; - } - - &-title { - position: absolute; - top: 0; - left: 0; - display: flex; - justify-content: center; - align-items: flex-start; - box-sizing: border-box; - width: 100%; - height: 2.2em; - padding: .5em; - background: $psv-gallery-title-background; - font: $psv-gallery-title-font; - line-height: 1.2em; - color: $psv-gallery-title-color; - z-index: 2; - transition: height ease-in-out .2s; - - span { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - user-select: none; - } - } - - &-thumb { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-position: center center; - background-size: cover; - transform: scale3d(1, 1, 1); - transition: transform ease-in-out .2s; - z-index: 1; - } - - &:hover &-title { - height: 100%; - - span { - white-space: normal; - } - } - - &:hover &-thumb { - transform: scale3d($psv-gallery-thumb-hover-scale, $psv-gallery-thumb-hover-scale, 1); - } - - &--active::after { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - box-sizing: border-box; - border: $psv-gallery-item-active-border; - z-index: 3; - } - } - - @media screen and (max-width: 500px) { - top: 0; - overflow-x: hidden; - overflow-y: auto; - - &-container { - flex-wrap: wrap; - margin-right: -$psv-gallery-padding; - } - - &-item { - width: calc(50% - #{$psv-gallery-padding}) !important; - margin-bottom: $psv-gallery-padding; - } - } -} diff --git a/src/plugins/gyroscope/DeviceOrientationControls.js b/src/plugins/gyroscope/DeviceOrientationControls.js deleted file mode 100644 index 228d16ed0..000000000 --- a/src/plugins/gyroscope/DeviceOrientationControls.js +++ /dev/null @@ -1,152 +0,0 @@ -import { - Euler, - MathUtils, - Quaternion, - Vector3 -} from 'three'; - -const _zee = new Vector3( 0, 0, 1 ); -const _euler = new Euler(); -const _q0 = new Quaternion(); -const _q1 = new Quaternion( - Math.sqrt( 0.5 ), 0, 0, Math.sqrt( 0.5 ) ); // - PI/2 around the x-axis - -/** - * Copied from three.js examples before deletion in r134 - * (deleted because of constructors/OS inconsistencies) - * @private - */ -class DeviceOrientationControls { - - constructor( object ) { - - if ( window.isSecureContext === false ) { - - console.error( 'THREE.DeviceOrientationControls: DeviceOrientationEvent is only available in secure contexts (https)' ); - - } - - const scope = this; - - const EPS = 0.000001; - const lastQuaternion = new Quaternion(); - - this.object = object; - this.object.rotation.reorder( 'YXZ' ); - - this.enabled = true; - - this.deviceOrientation = {}; - this.screenOrientation = 0; - - this.alphaOffset = 0; // radians - - const onDeviceOrientationChangeEvent = function ( event ) { - - scope.deviceOrientation = event; - - }; - - const onScreenOrientationChangeEvent = function () { - - scope.screenOrientation = window.orientation || 0; - - }; - - // The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y'' - - const setObjectQuaternion = function ( quaternion, alpha, beta, gamma, orient ) { - - _euler.set( beta, alpha, - gamma, 'YXZ' ); // 'ZXY' for the device, but 'YXZ' for us - - quaternion.setFromEuler( _euler ); // orient the device - - quaternion.multiply( _q1 ); // camera looks out the back of the device, not the top - - quaternion.multiply( _q0.setFromAxisAngle( _zee, - orient ) ); // adjust for screen orientation - - }; - - this.connect = function () { - - onScreenOrientationChangeEvent(); // run once on load - - // iOS 13+ - - if ( window.DeviceOrientationEvent !== undefined && typeof window.DeviceOrientationEvent.requestPermission === 'function' ) { - - window.DeviceOrientationEvent.requestPermission().then( function ( response ) { - - if ( response == 'granted' ) { - - window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent ); - window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent ); - - } - - } ).catch( function ( error ) { - - console.error( 'THREE.DeviceOrientationControls: Unable to use DeviceOrientation API:', error ); - - } ); - - } else { - - window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent ); - window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent ); - - } - - scope.enabled = true; - - }; - - this.disconnect = function () { - - window.removeEventListener( 'orientationchange', onScreenOrientationChangeEvent ); - window.removeEventListener( 'deviceorientation', onDeviceOrientationChangeEvent ); - - scope.enabled = false; - - }; - - this.update = function () { - - if ( scope.enabled === false ) return; - - const device = scope.deviceOrientation; - - if ( device ) { - - const alpha = device.alpha ? MathUtils.degToRad( device.alpha ) + scope.alphaOffset : 0; // Z - - const beta = device.beta ? MathUtils.degToRad( device.beta ) : 0; // X' - - const gamma = device.gamma ? MathUtils.degToRad( device.gamma ) : 0; // Y'' - - const orient = scope.screenOrientation ? MathUtils.degToRad( scope.screenOrientation ) : 0; // O - - setObjectQuaternion( scope.object.quaternion, alpha, beta, gamma, orient ); - - if ( 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { - - lastQuaternion.copy( scope.object.quaternion ); - - } - - } - - }; - - this.dispose = function () { - - scope.disconnect(); - - }; - - this.connect(); - - } - -} - -export { DeviceOrientationControls }; diff --git a/src/plugins/gyroscope/GyroscopeButton.js b/src/plugins/gyroscope/GyroscopeButton.js deleted file mode 100644 index b049dc525..000000000 --- a/src/plugins/gyroscope/GyroscopeButton.js +++ /dev/null @@ -1,72 +0,0 @@ -import { AbstractButton } from '../..'; -import compass from './compass.svg'; -import { EVENTS } from './constants'; - -/** - * @summary Navigation bar gyroscope button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class GyroscopeButton extends AbstractButton { - - static id = 'gyroscope'; - static icon = compass; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-gyroscope-button', true); - - /** - * @type {PSV.plugins.GyroscopePlugin} - * @readonly - * @private - */ - this.plugin = this.psv.getPlugin('gyroscope'); - - if (this.plugin) { - this.plugin.on(EVENTS.GYROSCOPE_UPDATED, this); - } - } - - /** - * @override - */ - destroy() { - if (this.plugin) { - this.plugin.off(EVENTS.GYROSCOPE_UPDATED, this); - } - - delete this.plugin; - - super.destroy(); - } - - /** - * @override - */ - isSupported() { - return !this.plugin ? false : { initial: false, promise: this.plugin.prop.isSupported }; - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - if (e.type === EVENTS.GYROSCOPE_UPDATED) { - this.toggleActive(e.args[0]); - } - } - - /** - * @override - * @description Toggles gyroscope control - */ - onClick() { - this.plugin.toggle(); - } - -} diff --git a/src/plugins/gyroscope/constants.js b/src/plugins/gyroscope/constants.js deleted file mode 100644 index a059a2600..000000000 --- a/src/plugins/gyroscope/constants.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @summary Available events - * @enum {string} - * @memberof PSV.plugins.GyroscopePlugin - * @constant - */ -export const EVENTS = { - /** - * @event gyroscope-updated - * @memberof PSV.plugins.GyroscopePlugin - * @summary Triggered when the gyroscope mode is enabled/disabled - * @param {boolean} enabled - */ - GYROSCOPE_UPDATED: 'gyroscope-updated', -}; diff --git a/src/plugins/gyroscope/index.js b/src/plugins/gyroscope/index.js deleted file mode 100644 index 5c6b04757..000000000 --- a/src/plugins/gyroscope/index.js +++ /dev/null @@ -1,311 +0,0 @@ -import { Object3D, Vector3 } from 'three'; -import { AbstractPlugin, CONSTANTS, DEFAULTS, registerButton, utils } from '../..'; -import { EVENTS } from './constants'; -import { DeviceOrientationControls } from './DeviceOrientationControls'; -import { GyroscopeButton } from './GyroscopeButton'; - - -/** - * @typedef {Object} PSV.plugins.GyroscopePlugin.Options - * @property {boolean} [touchmove=true] - allows to pan horizontally when the gyroscope is enabled (requires global `mousemove=true`) - * @property {boolean} [absolutePosition=false] - when true the view will ignore the current direction when enabling gyroscope control - * @property {'smooth' | 'fast'} [moveMode='smooth'] - How the gyroscope data is used to rotate the panorama. - */ - - -// add gyroscope button -DEFAULTS.lang[GyroscopeButton.id] = 'Gyroscope'; -registerButton(GyroscopeButton, 'caption:right'); - - -export { EVENTS } from './constants'; - - -const direction = new Vector3(); - - -/** - * @summary Adds gyroscope controls on mobile devices - * @extends PSV.plugins.AbstractPlugin - * @memberof PSV.plugins - */ -export class GyroscopePlugin extends AbstractPlugin { - - static id = 'gyroscope'; - - static EVENTS = EVENTS; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.plugins.GyroscopePlugin.Options} options - */ - constructor(psv, options) { - super(psv); - - /** - * @member {Object} - * @private - * @property {Promise} isSupported - indicates of the gyroscope API is available - * @property {number} alphaOffset - current alpha offset for gyroscope controls - * @property {boolean} enabled - * @property {boolean} config_moveInertia - original config "moveInertia" - */ - this.prop = { - isSupported : this.__checkSupport(), - alphaOffset : 0, - enabled : false, - config_moveInertia: true, - }; - - /** - * @member {PSV.plugins.GyroscopePlugin.Options} - * @private - */ - this.config = { - touchmove : true, - absolutePosition: false, - moveMode: 'smooth', - ...options, - }; - - /** - * @member {DeviceOrientationControls} - * @private - */ - this.controls = null; - } - - /** - * @package - */ - init() { - super.init(); - - this.psv.on(CONSTANTS.EVENTS.STOP_ALL, this); - this.psv.on(CONSTANTS.EVENTS.BEFORE_ROTATE, this); - this.psv.on(CONSTANTS.EVENTS.BEFORE_RENDER, this); - } - - /** - * @package - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.STOP_ALL, this); - this.psv.off(CONSTANTS.EVENTS.BEFORE_ROTATE, this); - this.psv.off(CONSTANTS.EVENTS.BEFORE_RENDER, this); - - this.stop(); - - delete this.controls; - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - switch (e.type) { - case CONSTANTS.EVENTS.STOP_ALL: - this.stop(); - break; - case CONSTANTS.EVENTS.BEFORE_RENDER: - this.__onBeforeRender(); - break; - case CONSTANTS.EVENTS.BEFORE_ROTATE: - this.__onBeforeRotate(e); - break; - default: - break; - } - } - - /** - * @summary Checks if the gyroscope is enabled - * @returns {boolean} - */ - isEnabled() { - return this.prop.enabled; - } - - /** - * @summary Enables the gyroscope navigation if available - * @returns {Promise} - * @fires PSV.plugins.GyroscopePlugin.gyroscope-updated - * @throws {PSV.PSVError} if the gyroscope API is not available/granted - */ - start() { - return this.prop.isSupported - .then((supported) => { - if (supported) { - return this.__requestPermission(); - } - else { - utils.logWarn('gyroscope not available'); - return Promise.reject(); - } - }) - .then((granted) => { - if (granted) { - return Promise.resolve(); - } - else { - utils.logWarn('gyroscope not allowed'); - return Promise.reject(); - } - }) - .then(() => { - this.psv.__stopAll(); - - // disable inertia - this.prop.config_moveInertia = this.psv.config.moveInertia; - this.psv.config.moveInertia = false; - - // enable gyro controls - if (!this.controls) { - this.controls = new DeviceOrientationControls(new Object3D()); - } - else { - this.controls.connect(); - } - - // force reset - this.controls.deviceOrientation = null; - this.controls.screenOrientation = 0; - this.controls.alphaOffset = 0; - - this.prop.alphaOffset = this.config.absolutePosition ? 0 : null; - this.prop.enabled = true; - - this.trigger(EVENTS.GYROSCOPE_UPDATED, true); - }); - } - - /** - * @summary Disables the gyroscope navigation - * @fires PSV.plugins.GyroscopePlugin.gyroscope-updated - */ - stop() { - if (this.isEnabled()) { - this.controls.disconnect(); - - this.prop.enabled = false; - this.psv.config.moveInertia = this.prop.config_moveInertia; - - this.trigger(EVENTS.GYROSCOPE_UPDATED, false); - - this.psv.resetIdleTimer(); - } - } - - /** - * @summary Enables or disables the gyroscope navigation - */ - toggle() { - if (this.isEnabled()) { - this.stop(); - } - else { - this.start(); - } - } - - /** - * @summary Handles gyro movements - * @private - */ - __onBeforeRender() { - if (!this.isEnabled()) { - return; - } - - if (!this.controls.deviceOrientation) { - return; - } - - const position = this.psv.getPosition(); - - // on first run compute the offset depending on the current viewer position and device orientation - if (this.prop.alphaOffset === null) { - this.controls.update(); - this.controls.object.getWorldDirection(direction); - - const sphericalCoords = this.psv.dataHelper.vector3ToSphericalCoords(direction); - this.prop.alphaOffset = sphericalCoords.longitude - position.longitude; - } - else { - this.controls.alphaOffset = this.prop.alphaOffset; - this.controls.update(); - this.controls.object.getWorldDirection(direction); - - const sphericalCoords = this.psv.dataHelper.vector3ToSphericalCoords(direction); - - const target = { - longitude: sphericalCoords.longitude, - latitude : -sphericalCoords.latitude, - }; - - // having a slow speed on smalls movements allows to absorb the device/hand vibrations - const step = this.config.moveMode === 'smooth' ? 3 : 10; - this.psv.dynamics.position.goto(target, utils.getAngle(position, target) < 0.01 ? 1 : step); - } - } - - /** - * @summary Intercepts moves and offsets the alpha angle - * @param {external:uEvent.Event} e - * @private - */ - __onBeforeRotate(e) { - if (this.isEnabled()) { - e.preventDefault(); - - if (this.config.touchmove) { - this.prop.alphaOffset -= e.args[0].longitude - this.psv.getPosition().longitude; - } - } - } - - /** - * @summary Detects if device orientation is supported - * @returns {Promise} - * @private - */ - __checkSupport() { - if ('DeviceMotionEvent' in window && typeof DeviceMotionEvent.requestPermission === 'function') { - return Promise.resolve(true); - } - else if ('DeviceOrientationEvent' in window) { - return new Promise((resolve) => { - const listener = (e) => { - resolve(e && e.alpha !== null && !isNaN(e.alpha)); - - window.removeEventListener('deviceorientation', listener); - }; - - window.addEventListener('deviceorientation', listener, false); - setTimeout(listener, 10000); - }); - } - else { - return Promise.resolve(false); - } - } - - /** - * @summary Request permission to the motion API - * @returns {Promise} - * @private - */ - __requestPermission() { - if ('DeviceMotionEvent' in window && typeof DeviceMotionEvent.requestPermission === 'function') { - return DeviceOrientationEvent.requestPermission() - .then(response => response === 'granted') - .catch(() => false); - } - else { - return Promise.resolve(true); - } - } - -} diff --git a/src/plugins/markers/Marker.js b/src/plugins/markers/Marker.js deleted file mode 100644 index ee2a8d251..000000000 --- a/src/plugins/markers/Marker.js +++ /dev/null @@ -1,761 +0,0 @@ -import { Group, MathUtils, Mesh, MeshBasicMaterial, PlaneGeometry, TextureLoader } from 'three'; -import { CONSTANTS, PSVError, utils } from '../..'; -import { MARKER_DATA, MARKER_TOOLTIP_TRIGGER, SVG_NS } from './constants'; -import { getPolygonCenter, getPolylineCenter } from './utils'; - -/** - * @summary Types of marker - * @memberOf PSV.plugins.MarkersPlugin - * @enum {string} - * @constant - * @private - */ -const MARKER_TYPES = { - image : 'image', - imageLayer : 'imageLayer', - html : 'html', - polygonPx : 'polygonPx', - polygonRad : 'polygonRad', - polylinePx : 'polylinePx', - polylineRad: 'polylineRad', - square : 'square', - rect : 'rect', - circle : 'circle', - ellipse : 'ellipse', - path : 'path', -}; - -/** - * @typedef {Object} PSV.plugins.MarkersPlugin.Properties - * @summary Marker properties, see {@link https://photo-sphere-viewer.js.org/plugins/plugin-markers.html#markers-options} - */ - -/** - * @summary Object representing a marker - * @memberOf PSV.plugins.MarkersPlugin - */ -export class Marker { - - /** - * @param {PSV.plugins.MarkersPlugin.Properties} properties - * @param {PSV.Viewer} psv - * @throws {PSV.PSVError} when the configuration is incorrect - */ - constructor(properties, psv) { - if (!properties.id) { - throw new PSVError('missing marker id'); - } - - /** - * @member {PSV.Viewer} - * @readonly - * @protected - */ - this.psv = psv; - - /** - * @member {string} - * @readonly - */ - this.id = properties.id; - - /** - * @member {string} - * @readonly - */ - this.type = Marker.getType(properties, false); - - /** - * @member {boolean} - * @protected - */ - this.visible = true; - - /** - * @member {HTMLElement|SVGElement|THREE.Object3D} - * @readonly - */ - this.$el = null; - - /** - * @summary Original configuration of the marker - * @member {PSV.plugins.MarkersPlugin.Properties} - * @readonly - */ - this.config = {}; - - /** - * @summary User data associated to the marker - * @member {any} - */ - this.data = undefined; - - /** - * @summary Tooltip instance for this marker - * @member {PSV.components.Tooltip} - */ - this.tooltip = null; - - /** - * @summary Computed properties - * @member {Object} - * @protected - * @property {boolean} dynamicSize - * @property {PSV.Point} anchor - * @property {boolean} visible - actually visible in the view - * @property {boolean} staticTooltip - the tooltip must always be shown - * @property {PSV.Position} position - position in spherical coordinates - * @property {PSV.Point} position2D - position in viewer coordinates - * @property {external:THREE.Vector3[]} positions3D - positions in 3D space - * @property {number} width - * @property {number} height - * @property {*} def - */ - this.props = { - dynamicSize : false, - anchor : null, - visible : false, - staticTooltip: false, - position : null, - position2D : null, - positions3D : null, - width : null, - height : null, - def : null, - }; - - /** - * @summary THREE file loader - * @type {THREE:TextureLoader} - * @private - */ - this.loader = null; - - if (this.is3d()) { - this.loader = new TextureLoader(); - if (this.psv.config.withCredentials) { - this.loader.setWithCredentials(true); - } - if (this.psv.config.requestHeaders && typeof this.psv.config.requestHeaders === 'object') { - this.loader.setRequestHeader(this.psv.config.requestHeaders); - } - } - - // create element - if (this.isNormal()) { - this.$el = document.createElement('div'); - } - else if (this.isPolygon()) { - this.$el = document.createElementNS(SVG_NS, 'polygon'); - } - else if (this.isPolyline()) { - this.$el = document.createElementNS(SVG_NS, 'polyline'); - } - else if (this.isSvg()) { - const svgType = this.type === 'square' ? 'rect' : this.type; - this.$el = document.createElementNS(SVG_NS, svgType); - } - - if (!this.is3d()) { - this.$el.id = `psv-marker-${this.id}`; - this.$el[MARKER_DATA] = this; - } - - this.update(properties); - } - - /** - * @summary Destroys the marker - */ - destroy() { - delete this.$el[MARKER_DATA]; - delete this.$el; - delete this.config; - delete this.props; - delete this.psv; - } - - /** - * @summary Checks if it is a 3D marker (imageLayer) - * @returns {boolean} - */ - is3d() { - return this.type === MARKER_TYPES.imageLayer; - } - - /** - * @summary Checks if it is a normal marker (image or html) - * @returns {boolean} - */ - isNormal() { - return this.type === MARKER_TYPES.image - || this.type === MARKER_TYPES.html; - } - - /** - * @summary Checks if it is a polygon/polyline marker - * @returns {boolean} - */ - isPoly() { - return this.isPolygon() - || this.isPolyline(); - } - - /** - * @summary Checks if it is a polygon/polyline using pixel coordinates - * @returns {boolean} - */ - isPolyPx() { - return this.type === MARKER_TYPES.polygonPx - || this.type === MARKER_TYPES.polylinePx; - } - - /** - * @summary Checks if it is a polygon/polyline using radian coordinates - * @returns {boolean} - */ - isPolyRad() { - return this.type === MARKER_TYPES.polygonRad - || this.type === MARKER_TYPES.polylineRad; - } - - /** - * @summary Checks if it is a polygon marker - * @returns {boolean} - */ - isPolygon() { - return this.type === MARKER_TYPES.polygonPx - || this.type === MARKER_TYPES.polygonRad; - } - - /** - * @summary Checks if it is a polyline marker - * @returns {boolean} - */ - isPolyline() { - return this.type === MARKER_TYPES.polylinePx - || this.type === MARKER_TYPES.polylineRad; - } - - /** - * @summary Checks if it is an SVG marker - * @returns {boolean} - */ - isSvg() { - return this.type === MARKER_TYPES.square - || this.type === MARKER_TYPES.rect - || this.type === MARKER_TYPES.circle - || this.type === MARKER_TYPES.ellipse - || this.type === MARKER_TYPES.path; - } - - /** - * @summary Computes marker scale from zoom level - * @param {number} zoomLevel - * @param {PSV.Position} position - * @returns {number} - */ - getScale(zoomLevel, position) { - if (!this.config.scale) { - return 1; - } - if (typeof this.config.scale === 'function') { - return this.config.scale(zoomLevel, position); - } - - let scale = 1; - if (Array.isArray(this.config.scale.zoom)) { - const bounds = this.config.scale.zoom; - scale *= bounds[0] + (bounds[1] - bounds[0]) * CONSTANTS.EASINGS.inQuad(zoomLevel / 100); - } - if (Array.isArray(this.config.scale.longitude)) { - const bounds = this.config.scale.longitude; - const halfFov = MathUtils.degToRad(this.psv.prop.hFov) / 2; - const arc = Math.abs(utils.getShortestArc(this.props.position.longitude, position.longitude)); - scale *= bounds[1] + (bounds[0] - bounds[1]) * CONSTANTS.EASINGS.outQuad(Math.max(0, (halfFov - arc) / halfFov)); - } - return scale; - } - - /** - * @summary Returns the markers list content for the marker, it can be either : - * - the `listContent` - * - the `tooltip.content` - * - the `html` - * - the `id` - * @returns {*} - */ - getListContent() { - if (this.config.listContent) { - return this.config.listContent; - } - else if (this.config.tooltip.content) { - return this.config.tooltip.content; - } - else if (this.config.html) { - return this.config.html; - } - else { - return this.id; - } - } - - /** - * @summary Display the tooltip of this marker - * @param {{clientX: number, clientY: number}} [mousePosition] - */ - showTooltip(mousePosition) { - if (this.props.visible && this.config.tooltip.content && this.props.position2D) { - const config = { - ...this.config.tooltip, - data: this, - }; - - if (this.isPoly()) { - if (mousePosition) { - const viewerPos = utils.getPosition(this.psv.container); - config.top = mousePosition.clientY - viewerPos.top; - config.left = mousePosition.clientX - viewerPos.left; - config.box = { // separate the tooltip from the cursor - width : 20, - height: 20, - }; - } - else { - config.top = this.props.position2D.y; - config.left = this.props.position2D.x; - } - } - else { - config.top = this.props.position2D.y + this.props.height / 2; - config.left = this.props.position2D.x + this.props.width / 2; - config.box = { - width : this.props.width, - height: this.props.height, - }; - } - - if (this.tooltip) { - this.tooltip.move(config); - } - else { - this.tooltip = this.psv.tooltip.create(config); - } - } - } - - /** - * @summary Recompute the position of the tooltip - */ - refreshTooltip() { - if (this.tooltip) { - this.showTooltip(); - } - } - - /** - * @summary Hides the tooltip of this marker - */ - hideTooltip() { - if (this.tooltip) { - this.tooltip.hide(); - this.tooltip = null; - } - } - - /** - * @summary Updates the marker with new properties - * @param {PSV.plugins.MarkersPlugin.Properties} properties - * @throws {PSV.PSVError} when the configuration is incorrect - */ - update(properties) { - const newType = Marker.getType(properties, true); - - if (newType !== undefined && newType !== this.type) { - throw new PSVError('cannot change marker type'); - } - - utils.deepmerge(this.config, properties); - if (typeof this.config.tooltip === 'string') { - this.config.tooltip = { content: this.config.tooltip }; - } - if (!this.config.tooltip) { - this.config.tooltip = {}; - } - if (!this.config.tooltip.trigger) { - this.config.tooltip.trigger = MARKER_TOOLTIP_TRIGGER.hover; - } - - this.data = this.config.data; - this.visible = this.config.visible !== false; - - if (!this.is3d()) { - // reset CSS class - if (this.isNormal()) { - this.$el.className = 'psv-marker psv-marker--normal'; - } - else { - this.$el.setAttribute('class', 'psv-marker psv-marker--svg'); - } - - // add CSS classes - if (this.config.className) { - utils.addClasses(this.$el, this.config.className); - } - - if (this.config.tooltip) { - this.$el.classList.add('psv-marker--has-tooltip'); - } - if (this.config.content) { - this.$el.classList.add('psv-marler--has-content'); - } - - // apply style - this.$el.style.opacity = this.config.opacity ?? 1; - if (this.config.style) { - utils.deepmerge(this.$el.style, this.config.style); - } - } - - // parse anchor - this.props.anchor = utils.parsePosition(this.config.anchor); - - // clean scale - if (this.config.scale && Array.isArray(this.config.scale)) { - this.config.scale = { zoom: this.config.scale }; - } - - if (this.isNormal()) { - this.__updateNormal(); - } - else if (this.isPoly()) { - this.__updatePoly(); - } - else if (this.isSvg()) { - this.__updateSvg(); - } - else if (this.is3d()) { - this.__update3d(); - } - } - - /** - * @summary Updates a normal marker - * @private - */ - __updateNormal() { - if (!utils.isExtendedPosition(this.config)) { - throw new PSVError('missing marker position, latitude/longitude or x/y'); - } - - if (this.config.image && (!this.config.width || !this.config.height)) { - throw new PSVError('missing marker width/height'); - } - - if (this.config.width && this.config.height) { - this.props.dynamicSize = false; - this.props.width = this.config.width; - this.props.height = this.config.height; - this.$el.style.width = this.config.width + 'px'; - this.$el.style.height = this.config.height + 'px'; - } - else { - this.props.dynamicSize = true; - } - - if (this.config.image) { - this.props.def = this.config.image; - this.$el.style.backgroundImage = `url(${this.config.image})`; - } - else if (this.config.html) { - this.props.def = this.config.html; - this.$el.innerHTML = this.config.html; - } - - // set anchor - this.$el.style.transformOrigin = `${this.props.anchor.x * 100}% ${this.props.anchor.y * 100}%`; - - // convert texture coordinates to spherical coordinates - this.props.position = this.psv.dataHelper.cleanPosition(this.config); - - // compute x/y/z position - this.props.positions3D = [this.psv.dataHelper.sphericalCoordsToVector3(this.props.position)]; - } - - /** - * @summary Updates an SVG marker - * @private - */ - __updateSvg() { - if (!utils.isExtendedPosition(this.config)) { - throw new PSVError('missing marker position, latitude/longitude or x/y'); - } - - this.props.dynamicSize = true; - - // set content - switch (this.type) { - case MARKER_TYPES.square: - this.props.def = { - x : 0, - y : 0, - width : this.config.square, - height: this.config.square, - }; - break; - - case MARKER_TYPES.rect: - if (Array.isArray(this.config.rect)) { - this.props.def = { - x : 0, - y : 0, - width : this.config.rect[0], - height: this.config.rect[1], - }; - } - else { - this.props.def = { - x : 0, - y : 0, - width : this.config.rect.width, - height: this.config.rect.height, - }; - } - break; - - case MARKER_TYPES.circle: - this.props.def = { - cx: this.config.circle, - cy: this.config.circle, - r : this.config.circle, - }; - break; - - case MARKER_TYPES.ellipse: - if (Array.isArray(this.config.ellipse)) { - this.props.def = { - cx: this.config.ellipse[0], - cy: this.config.ellipse[1], - rx: this.config.ellipse[0], - ry: this.config.ellipse[1], - }; - } - else { - this.props.def = { - cx: this.config.ellipse.rx, - cy: this.config.ellipse.ry, - rx: this.config.ellipse.rx, - ry: this.config.ellipse.ry, - }; - } - break; - - case MARKER_TYPES.path: - this.props.def = { - d: this.config.path, - }; - break; - - // no default - } - - utils.each(this.props.def, (value, prop) => { - this.$el.setAttributeNS(null, prop, value); - }); - - // set style - if (this.config.svgStyle) { - utils.each(this.config.svgStyle, (value, prop) => { - this.$el.setAttributeNS(null, utils.dasherize(prop), value); - }); - } - else { - this.$el.setAttributeNS(null, 'fill', 'rgba(0,0,0,0.5)'); - } - - // convert texture coordinates to spherical coordinates - this.props.position = this.psv.dataHelper.cleanPosition(this.config); - - // compute x/y/z position - this.props.positions3D = [this.psv.dataHelper.sphericalCoordsToVector3(this.props.position)]; - } - - /** - * @summary Updates a polygon marker - * @private - */ - __updatePoly() { - this.props.dynamicSize = true; - - // set style - if (this.config.svgStyle) { - utils.each(this.config.svgStyle, (value, prop) => { - this.$el.setAttributeNS(null, utils.dasherize(prop), value); - }); - - if (this.isPolyline() && !this.config.svgStyle.fill) { - this.$el.setAttributeNS(null, 'fill', 'none'); - } - } - else if (this.isPolygon()) { - this.$el.setAttributeNS(null, 'fill', 'rgba(0,0,0,0.5)'); - } - else if (this.isPolyline()) { - this.$el.setAttributeNS(null, 'fill', 'none'); - this.$el.setAttributeNS(null, 'stroke', 'rgb(0,0,0)'); - } - - // fold arrays: [1,2,3,4] => [[1,2],[3,4]] - const actualPoly = this.config.polygonPx || this.config.polygonRad || this.config.polylinePx || this.config.polylineRad; - if (!Array.isArray(actualPoly[0])) { - for (let i = 0; i < actualPoly.length; i++) { - actualPoly.splice(i, 2, [actualPoly[i], actualPoly[i + 1]]); - } - } - - // convert texture coordinates to spherical coordinates - if (this.isPolyPx()) { - this.props.def = actualPoly.map((coord) => { - const sphericalCoords = this.psv.dataHelper.textureCoordsToSphericalCoords({ x: coord[0], y: coord[1] }); - return [sphericalCoords.longitude, sphericalCoords.latitude]; - }); - } - // clean angles - else { - this.props.def = actualPoly.map((coord) => { - return [utils.parseAngle(coord[0]), utils.parseAngle(coord[1], true)]; - }); - } - - const centroid = this.isPolygon() - ? getPolygonCenter(this.props.def) - : getPolylineCenter(this.props.def); - - this.props.position = { - longitude: centroid[0], - latitude : centroid[1], - }; - - // compute x/y/z positions - this.props.positions3D = this.props.def.map((coord) => { - return this.psv.dataHelper.sphericalCoordsToVector3({ longitude: coord[0], latitude: coord[1] }); - }); - } - - /** - * @summary Updates a 3D marker - * @private - */ - __update3d() { - if (!this.config.width || !this.config.height) { - throw new PSVError('missing marker width/height'); - } - - this.props.dynamicSize = false; - this.props.width = this.config.width; - this.props.height = this.config.height; - - // convert texture coordinates to spherical coordinates - this.props.position = this.psv.dataHelper.cleanPosition(this.config); - - // compute x/y/z position - this.props.positions3D = [this.psv.dataHelper.sphericalCoordsToVector3(this.props.position)]; - - switch (this.type) { - case MARKER_TYPES.imageLayer: - if (!this.$el) { - const material = new MeshBasicMaterial({ - transparent: true, - opacity : this.config.opacity ?? 1, - depthTest : false, - }); - const geometry = new PlaneGeometry(1, 1); - const mesh = new Mesh(geometry, material); - mesh.userData = { [MARKER_DATA]: this }; - this.$el = new Group().add(mesh); - - // overwrite the visible property to be tied to the Marker instance - // and do it without context bleed - Object.defineProperty(this.$el, 'visible', { - enumerable: true, - get : function () { - return this.children[0].userData[MARKER_DATA].visible; - }, - set : function (visible) { - this.children[0].userData[MARKER_DATA].visible = visible; - }, - }); - } - - if (this.props.def !== this.config.imageLayer) { - if (this.psv.config.requestHeaders && typeof this.psv.config.requestHeaders === 'function') { - this.loader.setRequestHeader(this.psv.config.requestHeaders(this.config.imageLayer)); - } - this.$el.children[0].material.map = this.loader.load(this.config.imageLayer, (texture) => { - texture.anisotropy = 4; - this.psv.needsUpdate(); - }); - this.props.def = this.config.imageLayer; - } - - this.$el.children[0].position.set( - this.props.anchor.x - 0.5, - this.props.anchor.y - 0.5, - 0 - ); - - this.$el.position.copy(this.props.positions3D[0]); - - switch (this.config.orientation) { - case 'horizontal': - this.$el.lookAt(0, this.$el.position.y, 0); - this.$el.rotateX(this.props.position.latitude < 0 ? -Math.PI / 2 : Math.PI / 2); - break; - case 'vertical-left': - this.$el.lookAt(0, 0, 0); - this.$el.rotateY(-Math.PI * 0.4); - break; - case 'vertical-right': - this.$el.lookAt(0, 0, 0); - this.$el.rotateY(Math.PI * 0.4); - break; - default: - this.$el.lookAt(0, 0, 0); - break; - } - - // 100 is magic number that gives a coherent size at default zoom level - this.$el.scale.set(this.config.width / 100, this.config.height / 100, 1); - break; - - // no default - } - } - - /** - * @summary Determines the type of a marker by the available properties - * @param {Marker.Properties} properties - * @param {boolean} [allowNone=false] - * @returns {string} - * @throws {PSV.PSVError} when the marker's type cannot be found - */ - static getType(properties, allowNone = false) { - const found = []; - - utils.each(MARKER_TYPES, (type) => { - if (properties[type]) { - found.push(type); - } - }); - - if (found.length === 0 && !allowNone) { - throw new PSVError(`missing marker content, either ${Object.keys(MARKER_TYPES).join(', ')}`); - } - else if (found.length > 1) { - throw new PSVError(`multiple marker content, either ${Object.keys(MARKER_TYPES).join(', ')}`); - } - - return found[0]; - } - -} diff --git a/src/plugins/markers/MarkersButton.js b/src/plugins/markers/MarkersButton.js deleted file mode 100644 index 3d6408c10..000000000 --- a/src/plugins/markers/MarkersButton.js +++ /dev/null @@ -1,84 +0,0 @@ -import { AbstractButton } from '../..'; -import { EVENTS } from './constants'; -import pin from './pin.svg'; - -/** - * @summary Navigation bar markers button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class MarkersButton extends AbstractButton { - - static id = 'markers'; - static icon = pin; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-markers-button', true); - - /** - * @type {PSV.plugins.MarkersPlugin} - */ - this.plugin = this.psv.getPlugin('markers'); - - if (this.plugin) { - this.plugin.on(EVENTS.SHOW_MARKERS, this); - this.plugin.on(EVENTS.HIDE_MARKERS, this); - - this.toggleActive(true); - } - - this.hide(); - } - - /** - * @override - */ - destroy() { - if (this.plugin) { - this.plugin.off(EVENTS.SHOW_MARKERS, this); - this.plugin.off(EVENTS.HIDE_MARKERS, this); - } - - super.destroy(); - } - - /** - * @override - */ - isSupported() { - return !!this.plugin; - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case EVENTS.SHOW_MARKERS: this.toggleActive(true); break; - case EVENTS.HIDE_MARKERS: this.toggleActive(false); break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @override - * @description Toggles markers - */ - onClick() { - if (this.plugin.prop.visible) { - this.plugin.hide(); - } - else { - this.plugin.show(); - } - } - -} diff --git a/src/plugins/markers/MarkersListButton.js b/src/plugins/markers/MarkersListButton.js deleted file mode 100644 index 71ff661e3..000000000 --- a/src/plugins/markers/MarkersListButton.js +++ /dev/null @@ -1,75 +0,0 @@ -import { AbstractButton, CONSTANTS } from '../..'; -import { ID_PANEL_MARKERS_LIST } from './constants'; -import pinList from './pin-list.svg'; - -/** - * @summary Navigation bar markers list button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class MarkersListButton extends AbstractButton { - - static id = 'markersList'; - static icon = pinList; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-markers-list-button', true); - - /** - * @type {PSV.plugins.MarkersPlugin} - */ - this.plugin = this.psv.getPlugin('markers'); - - if (this.plugin) { - this.psv.on(CONSTANTS.EVENTS.OPEN_PANEL, this); - this.psv.on(CONSTANTS.EVENTS.CLOSE_PANEL, this); - } - - this.hide(); - } - - /** - * @override - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.OPEN_PANEL, this); - this.psv.off(CONSTANTS.EVENTS.CLOSE_PANEL, this); - - super.destroy(); - } - - /** - * @override - */ - isSupported() { - return !!this.plugin; - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case CONSTANTS.EVENTS.OPEN_PANEL: this.toggleActive(e.args[0] === ID_PANEL_MARKERS_LIST); break; - case CONSTANTS.EVENTS.CLOSE_PANEL: this.toggleActive(false); break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @override - * @description Toggles markers list - */ - onClick() { - this.plugin.toggleMarkersList(); - } - -} diff --git a/src/plugins/markers/constants.js b/src/plugins/markers/constants.js deleted file mode 100644 index 54a2f6224..000000000 --- a/src/plugins/markers/constants.js +++ /dev/null @@ -1,158 +0,0 @@ -import { utils } from '../..'; -import icon from './pin-list.svg'; - -/** - * @summary Available events - * @enum {string} - * @memberof PSV.plugins.MarkersPlugin - * @constant - */ -export const EVENTS = { - /** - * @event marker-visibility - * @memberof PSV.plugins.MarkersPlugin - * @summary Triggered when the visibility of a marker changes - * @param {PSV.plugins.MarkersPlugin.Marker} marker - * @param {boolean} visible - */ - MARKER_VISIBILITY : 'marker-visibility', - /** - * @event goto-marker-done - * @memberof PSV.plugins.MarkersPlugin - * @summary Triggered when the animation to a marker is done - * @param {PSV.plugins.MarkersPlugin.Marker} marker - */ - GOTO_MARKER_DONE : 'goto-marker-done', - /** - * @event leave-marker - * @memberof PSV.plugins.MarkersPlugin - * @summary Triggered when the user puts the cursor away from a marker - * @param {PSV.plugins.MarkersPlugin.Marker} marker - */ - LEAVE_MARKER : 'leave-marker', - /** - * @event over-marker - * @memberof PSV.plugins.MarkersPlugin - * @summary Triggered when the user puts the cursor hover a marker - * @param {PSV.plugins.MarkersPlugin.Marker} marker - */ - OVER_MARKER : 'over-marker', - /** - * @event filter:render-markers-list - * @memberof PSV.plugins.MarkersPlugin - * @summary Used to alter the list of markers displayed on the side-panel - * @param {PSV.plugins.MarkersPlugin.Marker[]} markers - * @returns {PSV.plugins.MarkersPlugin.Marker[]} - */ - RENDER_MARKERS_LIST: 'render-markers-list', - /** - * @event select-marker - * @memberof PSV.plugins.MarkersPlugin - * @summary Triggered when the user clicks on a marker. The marker can be retrieved from outside the event handler - * with {@link PSV.plugins.MarkersPlugin.getCurrentMarker} - * @param {PSV.plugins.MarkersPlugin.Marker} marker - * @param {PSV.plugins.MarkersPlugin.SelectMarkerData} data - */ - SELECT_MARKER : 'select-marker', - /** - * @event select-marker-list - * @memberof PSV.plugins.MarkersPlugin - * @summary Triggered when a marker is selected from the side panel - * @param {PSV.plugins.MarkersPlugin.Marker} marker - */ - SELECT_MARKER_LIST : 'select-marker-list', - /** - * @event unselect-marker - * @memberof PSV.plugins.MarkersPlugin - * @summary Triggered when a marker was selected and the user clicks elsewhere - * @param {PSV.plugins.MarkersPlugin.Marker} marker - */ - UNSELECT_MARKER : 'unselect-marker', - /** - * @event hide-markers - * @memberof PSV.plugins.MarkersPlugin - * @summary Triggered when the markers are hidden - */ - HIDE_MARKERS : 'hide-markers', - /** - * @event set-marker - * @memberof PSV.plugins.MarkersPlugin - * @summary Triggered when the list of markers changes - * @param {PSV.plugins.MarkersPlugin.Marker[]} markers - */ - SET_MARKERS : 'set-markers', - /** - * @event show-markers - * @memberof PSV.plugins.MarkersPlugin - * @summary Triggered when the markers are shown - */ - SHOW_MARKERS : 'show-markers', -}; - -/** - * @summary Types of tooltip events - * @memberOf PSV.plugins.MarkersPlugin - * @enum {string} - * @constant - * @private - */ -export const MARKER_TOOLTIP_TRIGGER = { - click: 'click', - hover: 'hover', -}; - -/** - * @summary Namespace for SVG creation - * @type {string} - * @constant - * @private - */ -export const SVG_NS = 'http://www.w3.org/2000/svg'; - -/** - * @summary Property name added to marker elements - * @type {string} - * @constant - * @private - */ -export const MARKER_DATA = 'psvMarker'; - -/** - * @summary Panel identifier for marker content - * @type {string} - * @constant - * @private - */ -export const ID_PANEL_MARKER = 'marker'; - -/** - * @summary Panel identifier for markers list - * @type {string} - * @constant - * @private - */ -export const ID_PANEL_MARKERS_LIST = 'markersList'; - -const MARKER_DATA_KEY = utils.dasherize(MARKER_DATA); - -/** - * @summary Markers list template - * @param {PSV.plugins.MarkersPlugin.Marker[]} markers - * @param {string} title - * @returns {string} - * @constant - * @private - */ -export const MARKERS_LIST_TEMPLATE = (markers, title) => ` -
-

${icon} ${title}

-
    - ${markers.map(marker => ` -
  • - ${marker.type === 'image' ? `` : ''} - ${marker.getListContent()} -
  • - `).join('')} -
-
-`; diff --git a/src/plugins/markers/index.js b/src/plugins/markers/index.js deleted file mode 100644 index ade2350d8..000000000 --- a/src/plugins/markers/index.js +++ /dev/null @@ -1,1017 +0,0 @@ -import { Vector3 } from 'three'; -import { AbstractPlugin, CONSTANTS, DEFAULTS, PSVError, registerButton, utils } from '../..'; -import { - EVENTS, - ID_PANEL_MARKER, - ID_PANEL_MARKERS_LIST, - MARKER_DATA, - MARKER_TOOLTIP_TRIGGER, - MARKERS_LIST_TEMPLATE, - SVG_NS -} from './constants'; -import { Marker } from './Marker'; -import { MarkersButton } from './MarkersButton'; -import { MarkersListButton } from './MarkersListButton'; -import './style.scss'; - - -/** - * @typedef {Object} PSV.plugins.MarkersPlugin.Options - * @property {boolean} [clickEventOnMarker=false] If a `click` event is triggered on the viewer additionally to the `select-marker` event. - * @property {string | number} [gotoMarkerSpeed=8rpm] Default animation speed for `gotoMarker` method - * @property {PSV.plugins.MarkersPlugin.Properties[]} [markers] - */ - -/** - * @typedef {Object} PSV.plugins.MarkersPlugin.SelectMarkerData - * @summary Data of the `select-marker` event - * @property {boolean} dblclick - if the selection originated from a double click, the simple click is always fired before the double click - * @property {boolean} rightclick - if the selection originated from a right click - */ - - -// add markers buttons -DEFAULTS.lang[MarkersButton.id] = 'Markers'; -DEFAULTS.lang[MarkersListButton.id] = 'Markers list'; -registerButton(MarkersButton, 'caption:left'); -registerButton(MarkersListButton, 'caption:left'); - - -export { EVENTS } from './constants'; - - -/** - * @summary Displays various markers on the viewer - * @extends PSV.plugins.AbstractPlugin - * @memberof PSV.plugins - */ -export class MarkersPlugin extends AbstractPlugin { - - static id = 'markers'; - - static EVENTS = EVENTS; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.plugins.MarkersPlugin.Options} [options] - */ - constructor(psv, options) { - super(psv); - - /** - * @summary All registered markers - * @member {Object} - */ - this.markers = {}; - - /** - * @type {Object} - * @property {boolean} visible - Visibility of the component - * @property {PSV.plugins.MarkersPlugin.Marker} currentMarker - Last selected marker - * @property {PSV.plugins.MarkersPlugin.Marker} hoveringMarker - Marker under the cursor - * @private - */ - this.prop = { - visible : true, - currentMarker : null, - hoveringMarker: null, - stopObserver : null, - }; - - /** - * @type {PSV.plugins.MarkersPlugin.Options} - */ - this.config = { - clickEventOnMarker: false, - gotoMarkerSpeed: '8rpm', - ...options, - }; - - /** - * @member {HTMLElement} - * @readonly - */ - this.container = document.createElement('div'); - this.container.className = 'psv-markers'; - this.container.style.cursor = this.psv.config.mousemove ? 'move' : 'default'; - - /** - * @member {SVGElement} - * @readonly - */ - this.svgContainer = document.createElementNS(SVG_NS, 'svg'); - this.svgContainer.setAttribute('class', 'psv-markers-svg-container'); - this.container.appendChild(this.svgContainer); - - // Markers events via delegation - this.container.addEventListener('mouseenter', this, true); - this.container.addEventListener('mouseleave', this, true); - this.container.addEventListener('mousemove', this, true); - this.container.addEventListener('contextmenu', this); - } - - /** - * @package - */ - init() { - super.init(); - - this.psv.container.appendChild(this.container); - - // Viewer events - this.psv.on(CONSTANTS.EVENTS.CLICK, this); - this.psv.on(CONSTANTS.EVENTS.DOUBLE_CLICK, this); - this.psv.on(CONSTANTS.EVENTS.RENDER, this); - this.psv.on(CONSTANTS.EVENTS.CONFIG_CHANGED, this); - - this.psv.once(CONSTANTS.EVENTS.READY, () => { - if (this.config.markers) { - this.setMarkers(this.config.markers); - delete this.config.markers; - } - }); - } - - /** - * @package - */ - destroy() { - this.clearMarkers(false); - - this.prop.stopObserver?.(); - - this.psv.off(CONSTANTS.EVENTS.CLICK, this); - this.psv.off(CONSTANTS.EVENTS.DOUBLE_CLICK, this); - this.psv.off(CONSTANTS.EVENTS.RENDER, this); - this.psv.off(CONSTANTS.EVENTS.CONFIG_CHANGED, this); - - this.psv.container.removeChild(this.container); - - delete this.svgContainer; - delete this.markers; - delete this.container; - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case 'mouseenter': this.__onMouseEnter(e, this.__getTargetMarker(e.target)); break; - case 'mouseleave': this.__onMouseLeave(e, this.__getTargetMarker(e.target)); break; - case 'mousemove': this.__onMouseMove(e, this.__getTargetMarker(e.target)); break; - case 'contextmenu': e.preventDefault(); break; - case CONSTANTS.EVENTS.CLICK: this.__onClick(e, e.args[0], false); break; - case CONSTANTS.EVENTS.DOUBLE_CLICK: this.__onClick(e, e.args[0], true); break; - case CONSTANTS.EVENTS.RENDER: this.renderMarkers(); break; - case CONSTANTS.OBJECT_EVENTS.ENTER_OBJECT: this.__onMouseEnter(e.detail.originalEvent, e.detail.data); break; - case CONSTANTS.OBJECT_EVENTS.LEAVE_OBJECT: this.__onMouseLeave(e.detail.originalEvent, e.detail.data); break; - case CONSTANTS.OBJECT_EVENTS.HOVER_OBJECT: this.__onMouseMove(e.detail.originalEvent, e.detail.data); break; - case CONSTANTS.EVENTS.CONFIG_CHANGED: - this.container.style.cursor = this.psv.config.mousemove ? 'move' : 'default'; - break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @summary Shows all markers - * @fires PSV.plugins.MarkersPlugin.show-markers - */ - show() { - this.prop.visible = true; - - this.renderMarkers(); - - this.trigger(EVENTS.SHOW_MARKERS); - } - - /** - * @summary Hides all markers - * @fires PSV.plugins.MarkersPlugin.hide-markers - */ - hide() { - this.prop.visible = false; - - this.renderMarkers(); - - this.trigger(EVENTS.HIDE_MARKERS); - } - - /** - * @summary Toggles the visibility of all tooltips - */ - toggleAllTooltips() { - if (this.prop.showAllTooltips) { - this.hideAllTooltips(); - } - else { - this.showAllTooltips(); - } - } - - /** - * @summary Displays all tooltips - */ - showAllTooltips() { - this.prop.showAllTooltips = true; - utils.each(this.markers, (marker) => { - marker.props.staticTooltip = true; - marker.showTooltip(); - }); - } - - /** - * @summary Hides all tooltips - */ - hideAllTooltips() { - this.prop.showAllTooltips = false; - utils.each(this.markers, (marker) => { - marker.props.staticTooltip = false; - marker.hideTooltip(); - }); - } - - /** - * @summary Returns the total number of markers - * @returns {number} - */ - getNbMarkers() { - return Object.keys(this.markers).length; - } - - /** - * @summary Returns all the markers - * @return {PSV.plugins.MarkersPlugin.Marker[]} - */ - getMarkers() { - return Object.values(this.markers); - } - - /** - * @summary Adds a new marker to viewer - * @param {PSV.plugins.MarkersPlugin.Properties} properties - * @param {boolean} [render=true] - renders the marker immediately - * @returns {PSV.plugins.MarkersPlugin.Marker} - * @throws {PSV.PSVError} when the marker's id is missing or already exists - */ - addMarker(properties, render = true) { - if (this.markers[properties.id]) { - throw new PSVError(`marker "${properties.id}" already exists`); - } - - const marker = new Marker(properties, this.psv); - - if (marker.isNormal()) { - this.container.appendChild(marker.$el); - } - else if (marker.isPoly() || marker.isSvg()) { - this.svgContainer.appendChild(marker.$el); - } - else if (marker.is3d()) { - this.psv.renderer.scene.add(marker.$el); - } - - this.markers[marker.id] = marker; - - if (render) { - this.renderMarkers(); - this.__refreshUi(); - this.__checkObjectsObserver(); - - this.trigger(EVENTS.SET_MARKERS, this.getMarkers()); - } - - return marker; - } - - /** - * @summary Returns the internal marker object for a marker id - * @param {string} markerId - * @returns {PSV.plugins.MarkersPlugin.Marker} - * @throws {PSV.PSVError} when the marker cannot be found - */ - getMarker(markerId) { - const id = typeof markerId === 'object' ? markerId.id : markerId; - - if (!this.markers[id]) { - throw new PSVError(`cannot find marker "${id}"`); - } - - return this.markers[id]; - } - - /** - * @summary Returns the last marker selected by the user - * @returns {PSV.plugins.MarkersPlugin.Marker} - */ - getCurrentMarker() { - return this.prop.currentMarker; - } - - /** - * @summary Updates the existing marker with the same id - * @description Every property can be changed but you can't change its type (Eg: `image` to `html`). - * @param {PSV.plugins.MarkersPlugin.Properties} properties - * @param {boolean} [render=true] - renders the marker immediately - * @returns {PSV.plugins.MarkersPlugin.Marker} - */ - updateMarker(properties, render = true) { - const marker = this.getMarker(properties.id); - - marker.update(properties); - - if (render) { - this.renderMarkers(); - this.__refreshUi(); - - if (marker.is3d()) { - this.psv.needsUpdate(); - } - - this.trigger(EVENTS.SET_MARKERS, this.getMarkers()); - } - - return marker; - } - - /** - * @summary Removes a marker from the viewer - * @param {string} markerId - * @param {boolean} [render=true] - renders the marker immediately - */ - removeMarker(markerId, render = true) { - const marker = this.getMarker(markerId); - - if (marker.isNormal()) { - this.container.removeChild(marker.$el); - } - else if (marker.isPoly() || marker.isSvg()) { - this.svgContainer.removeChild(marker.$el); - } - else if (marker.is3d()) { - this.psv.renderer.scene.remove(marker.$el); - this.psv.needsUpdate(); - } - - if (this.prop.hoveringMarker === marker) { - this.prop.hoveringMarker = null; - } - - if (this.prop.currentMarker === marker) { - this.prop.currentMarker = null; - } - - marker.hideTooltip(); - - marker.destroy(); - delete this.markers[marker.id]; - - if (render) { - this.__refreshUi(); - this.__checkObjectsObserver(); - - this.trigger(EVENTS.SET_MARKERS, this.getMarkers()); - } - } - - /** - * @summary Removes multiple markers - * @param {string[]} markerIds - * @param {boolean} [render=true] - renders the markers immediately - */ - removeMarkers(markerIds, render = true) { - markerIds.forEach(markerId => this.removeMarker(markerId, false)); - - if (render) { - this.__refreshUi(); - this.__checkObjectsObserver(); - - this.trigger(EVENTS.SET_MARKERS, this.getMarkers()); - } - } - - /** - * @summary Replaces all markers - * @param {PSV.plugins.MarkersPlugin.Properties[]} markers - * @param {boolean} [render=true] - renders the marker immediately - */ - setMarkers(markers, render = true) { - this.clearMarkers(false); - - utils.each(markers, marker => this.addMarker(marker, false)); - - if (render) { - this.renderMarkers(); - this.__refreshUi(); - this.__checkObjectsObserver(); - - this.trigger(EVENTS.SET_MARKERS, this.getMarkers()); - } - } - - /** - * @summary Removes all markers - * @param {boolean} [render=true] - renders the markers immediately - */ - clearMarkers(render = true) { - utils.each(this.markers, marker => this.removeMarker(marker, false)); - - if (render) { - this.renderMarkers(); - this.__refreshUi(); - this.__checkObjectsObserver(); - - this.trigger(EVENTS.SET_MARKERS, this.getMarkers()); - } - } - - /** - * @summary Rotate the view to face the marker - * @param {string} markerId - * @param {string|number} [speed] - rotates smoothy, see {@link PSV.Viewer#animate} - * @fires PSV.plugins.MarkersPlugin.goto-marker-done - * @return {PSV.utils.Animation} A promise that will be resolved when the animation finishes - */ - gotoMarker(markerId, speed = this.config.gotoMarkerSpeed) { - const marker = this.getMarker(markerId); - - return this.psv.animate({ - ...marker.props.position, - zoom : marker.config.zoomLvl, - speed: speed, - }) - .then(() => { - this.trigger(EVENTS.GOTO_MARKER_DONE, marker); - }); - } - - /** - * @summary Hides a marker - * @param {string} markerId - */ - hideMarker(markerId) { - this.toggleMarker(markerId, false); - } - - /** - * @summary Shows a marker - * @param {string} markerId - */ - showMarker(markerId) { - this.toggleMarker(markerId, true); - } - - /** - * @summary Forces the display of the tooltip - * @param {string} markerId - */ - showMarkerTooltip(markerId) { - const marker = this.getMarker(markerId); - marker.props.staticTooltip = true; - marker.showTooltip(); - } - - /** - * @summary Hides the tooltip - * @param {string} markerId - */ - hideMarkerTooltip(markerId) { - const marker = this.getMarker(markerId); - marker.props.staticTooltip = false; - marker.hideTooltip(); - } - - /** - * @summary Toggles a marker - * @param {string} markerId - * @param {boolean} [visible] - */ - toggleMarker(markerId, visible = null) { - const marker = this.getMarker(markerId); - marker.visible = visible === null ? !marker.visible : visible; - if (marker.is3d()) { - this.psv.needsUpdate(); - } - else { - this.renderMarkers(); - } - } - - /** - * @summary Opens the panel with the content of the marker - * @param {string} markerId - */ - showMarkerPanel(markerId) { - const marker = this.getMarker(markerId); - - if (marker?.config?.content) { - this.psv.panel.show({ - id : ID_PANEL_MARKER, - content: marker.config.content, - }); - } - else { - this.psv.panel.hide(ID_PANEL_MARKER); - } - } - - /** - * @summary Toggles the visibility of the list of markers - */ - toggleMarkersList() { - if (this.psv.panel.prop.contentId === ID_PANEL_MARKERS_LIST) { - this.hideMarkersList(); - } - else { - this.showMarkersList(); - } - } - - /** - * @summary Opens side panel with the list of markers - * @fires PSV.plugins.MarkersPlugin.filter:render-markers-list - */ - showMarkersList() { - let markers = []; - utils.each(this.markers, (marker) => { - if (marker.visible && !marker.config.hideList) { - markers.push(marker); - } - }); - - markers = this.change(EVENTS.RENDER_MARKERS_LIST, markers); - - this.psv.panel.show({ - id : ID_PANEL_MARKERS_LIST, - content : MARKERS_LIST_TEMPLATE(markers, this.psv.config.lang[MarkersButton.id]), - noMargin : true, - clickHandler: (e) => { - const li = e.target ? utils.getClosest(e.target, 'li') : undefined; - const markerId = li ? li.dataset[MARKER_DATA] : undefined; - - if (markerId) { - const marker = this.getMarker(markerId); - - this.trigger(EVENTS.SELECT_MARKER_LIST, marker); - - this.gotoMarker(marker); - this.hideMarkersList(); - } - }, - }); - } - - /** - * @summary Closes side panel if it contains the list of markers - */ - hideMarkersList() { - this.psv.panel.hide(ID_PANEL_MARKERS_LIST); - } - - /** - * @summary Updates the visibility and the position of all markers - */ - renderMarkers() { - const zoomLevel = this.psv.getZoomLevel(); - const viewerPosition = this.psv.getPosition(); - - utils.each(this.markers, (marker) => { - let isVisible = this.prop.visible && marker.visible; - let visibilityChanged = false; - let position = null; - - if (isVisible && marker.is3d()) { - position = this.__getMarkerPosition(marker); - isVisible = this.__isMarkerVisible(marker, position); - } - else if (isVisible && marker.isPoly()) { - const positions = this.__getPolyPositions(marker); - isVisible = positions.length > (marker.isPolygon() ? 2 : 1); - - if (isVisible) { - position = this.__getMarkerPosition(marker); - - const points = positions.map(pos => (pos.x - position.x) + ',' + (pos.y - position.y)).join(' '); - - marker.$el.setAttributeNS(null, 'points', points); - marker.$el.setAttributeNS(null, 'transform', `translate(${position.x} ${position.y})`); - } - } - else if (isVisible) { - if (marker.props.dynamicSize) { - this.__updateMarkerSize(marker); - } - - position = this.__getMarkerPosition(marker); - isVisible = this.__isMarkerVisible(marker, position); - - if (isVisible) { - const scale = marker.getScale(zoomLevel, viewerPosition); - - if (marker.isSvg()) { - // simulate transform-origin relative to SVG element - const x = position.x + marker.props.width * marker.props.anchor.x * (1 - scale); - const y = position.y + marker.props.height * marker.props.anchor.y * (1 - scale); - marker.$el.setAttributeNS(null, 'transform', `translate(${x}, ${y}) scale(${scale}, ${scale})`); - } - else { - marker.$el.style.transform = `translate3D(${position.x}px, ${position.y}px, 0px) scale(${scale}, ${scale})`; - } - } - } - - visibilityChanged = marker.props.visible !== isVisible; - marker.props.visible = isVisible; - marker.props.position2D = isVisible ? position : null; - - if (!marker.is3d()) { - utils.toggleClass(marker.$el, 'psv-marker--visible', isVisible); - } - - if (!isVisible) { - marker.hideTooltip(); - } - else if (marker.props.staticTooltip) { - marker.showTooltip(); - } - else if (marker.config.tooltip.trigger === MARKER_TOOLTIP_TRIGGER.click || (marker === this.prop.hoveringMarker && !marker.isPoly())) { - marker.refreshTooltip(); - } - else if (marker !== this.prop.hoveringMarker) { - marker.hideTooltip(); - } - - if (visibilityChanged) { - this.trigger(EVENTS.MARKER_VISIBILITY, marker, isVisible); - } - }); - } - - /** - * @summary Determines if a point marker is visible
- * It tests if the point is in the general direction of the camera, then check if it's in the viewport - * @param {PSV.plugins.MarkersPlugin.Marker} marker - * @param {PSV.Point} position - * @returns {boolean} - * @private - */ - __isMarkerVisible(marker, position) { - return marker.props.positions3D[0].dot(this.psv.prop.direction) > 0 - && position.x + marker.props.width >= 0 - && position.x - marker.props.width <= this.psv.prop.size.width - && position.y + marker.props.height >= 0 - && position.y - marker.props.height <= this.psv.prop.size.height; - } - - /** - * @summary Computes the real size of a marker - * @description This is done by removing all it's transformations (if any) and making it visible - * before querying its bounding rect - * @param {PSV.plugins.MarkersPlugin.Marker} marker - * @private - */ - __updateMarkerSize(marker) { - marker.$el.classList.add('psv-marker--transparent'); - - let transform; - if (marker.isSvg()) { - transform = marker.$el.getAttributeNS(null, 'transform'); - marker.$el.removeAttributeNS(null, 'transform'); - } - else { - transform = marker.$el.style.transform; - marker.$el.style.transform = ''; - } - - const rect = marker.$el.getBoundingClientRect(); - marker.props.width = rect.width; - marker.props.height = rect.height; - - marker.$el.classList.remove('psv-marker--transparent'); - - if (transform) { - if (marker.isSvg()) { - marker.$el.setAttributeNS(null, 'transform', transform); - } - else { - marker.$el.style.transform = transform; - } - } - - // the size is no longer dynamic once known - marker.props.dynamicSize = false; - } - - /** - * @summary Computes viewer coordinates of a marker - * @param {PSV.plugins.MarkersPlugin.Marker} marker - * @returns {PSV.Point} - * @private - */ - __getMarkerPosition(marker) { - if (marker.isPoly()) { - return this.psv.dataHelper.sphericalCoordsToViewerCoords(marker.props.position); - } - else { - const position = this.psv.dataHelper.vector3ToViewerCoords(marker.props.positions3D[0]); - - position.x -= marker.props.width * marker.props.anchor.x; - position.y -= marker.props.height * marker.props.anchor.y; - - return position; - } - } - - /** - * @summary Computes viewer coordinates of each point of a polygon/polyline
- * It handles points behind the camera by creating intermediary points suitable for the projector - * @param {PSV.plugins.MarkersPlugin.Marker} marker - * @returns {PSV.Point[]} - * @private - */ - __getPolyPositions(marker) { - const nbVectors = marker.props.positions3D.length; - - // compute if each vector is visible - const positions3D = marker.props.positions3D.map((vector) => { - return { - vector : vector, - visible: vector.dot(this.psv.prop.direction) > 0, - }; - }); - - // get pairs of visible/invisible vectors for each invisible vector connected to a visible vector - const toBeComputed = []; - positions3D.forEach((pos, i) => { - if (!pos.visible) { - const neighbours = [ - i === 0 ? positions3D[nbVectors - 1] : positions3D[i - 1], - i === nbVectors - 1 ? positions3D[0] : positions3D[i + 1], - ]; - - neighbours.forEach((neighbour) => { - if (neighbour.visible) { - toBeComputed.push({ - visible : neighbour, - invisible: pos, - index : i, - }); - } - }); - } - }); - - // compute intermediary vector for each pair (the loop is reversed for splice to insert at the right place) - toBeComputed.reverse().forEach((pair) => { - positions3D.splice(pair.index, 0, { - vector : this.__getPolyIntermediaryPoint(pair.visible.vector, pair.invisible.vector), - visible: true, - }); - }); - - // translate vectors to screen pos - return positions3D - .filter(pos => pos.visible) - .map(pos => this.psv.dataHelper.vector3ToViewerCoords(pos.vector)); - } - - /** - * Given one point in the same direction of the camera and one point behind the camera, - * computes an intermediary point on the great circle delimiting the half sphere visible by the camera. - * The point is shifted by .01 rad because the projector cannot handle points exactly on this circle. - * TODO : does not work with fisheye view (must not use the great circle) - * {@link http://math.stackexchange.com/a/1730410/327208} - * @param P1 {external:THREE.Vector3} - * @param P2 {external:THREE.Vector3} - * @returns {external:THREE.Vector3} - * @private - */ - __getPolyIntermediaryPoint(P1, P2) { - const C = this.psv.prop.direction.clone().normalize(); - const N = new Vector3().crossVectors(P1, P2).normalize(); - const V = new Vector3().crossVectors(N, P1).normalize(); - const X = P1.clone().multiplyScalar(-C.dot(V)); - const Y = V.clone().multiplyScalar(C.dot(P1)); - const H = new Vector3().addVectors(X, Y).normalize(); - const a = new Vector3().crossVectors(H, C); - return H.applyAxisAngle(a, 0.01).multiplyScalar(CONSTANTS.SPHERE_RADIUS); - } - - /** - * @summary Returns the marker associated to an event target - * @param {EventTarget} target - * @param {boolean} [closest=false] - * @returns {PSV.plugins.MarkersPlugin.Marker} - * @private - */ - __getTargetMarker(target, closest = false) { - const target2 = closest ? utils.getClosest(target, '.psv-marker') : target; - return target2 ? target2[MARKER_DATA] : undefined; - } - - /** - * @summary Checks if an event target is in the tooltip - * @param {EventTarget} target - * @param {PSV.components.Tooltip} tooltip - * @returns {boolean} - * @private - */ - __targetOnTooltip(target, tooltip) { - return target && tooltip ? utils.hasParent(target, tooltip.container) : false; - } - - /** - * @summary Handles mouse enter events, show the tooltip for non polygon markers - * @param {MouseEvent} e - * @param {PSV.plugins.MarkersPlugin.Marker} [marker] - * @fires PSV.plugins.MarkersPlugin.over-marker - * @private - */ - __onMouseEnter(e, marker) { - if (marker && !marker.isPoly()) { - this.prop.hoveringMarker = marker; - - this.trigger(EVENTS.OVER_MARKER, marker); - - if (!marker.props.staticTooltip && marker.config.tooltip.trigger === MARKER_TOOLTIP_TRIGGER.hover) { - marker.showTooltip(e); - } - } - } - - /** - * @summary Handles mouse leave events, hide the tooltip - * @param {MouseEvent} e - * @param {PSV.plugins.MarkersPlugin.Marker} [marker] - * @fires PSV.plugins.MarkersPlugin.leave-marker - * @private - */ - __onMouseLeave(e, marker) { - // do not hide if we enter the tooltip itself while hovering a polygon - if (marker && !(marker.isPoly() && this.__targetOnTooltip(e.relatedTarget, marker.tooltip))) { - this.trigger(EVENTS.LEAVE_MARKER, marker); - - this.prop.hoveringMarker = null; - - if (!marker.props.staticTooltip && marker.config.tooltip.trigger === MARKER_TOOLTIP_TRIGGER.hover) { - marker.hideTooltip(); - } - } - } - - /** - * @summary Handles mouse move events, refreshUi the tooltip for polygon markers - * @param {MouseEvent} e - * @param {PSV.plugins.MarkersPlugin.Marker} [targetMarker] - * @fires PSV.plugins.MarkersPlugin.leave-marker - * @fires PSV.plugins.MarkersPlugin.over-marker - * @private - */ - __onMouseMove(e, targetMarker) { - let marker; - - if (targetMarker?.isPoly()) { - marker = targetMarker; - } - // do not hide if we enter the tooltip itself while hovering a polygon - else if (this.prop.hoveringMarker && this.__targetOnTooltip(e.target, this.prop.hoveringMarker.tooltip)) { - marker = this.prop.hoveringMarker; - } - - if (marker) { - if (!this.prop.hoveringMarker) { - this.trigger(EVENTS.OVER_MARKER, marker); - - this.prop.hoveringMarker = marker; - } - - if (!marker.props.staticTooltip) { - marker.showTooltip(e); - } - } - else if (this.prop.hoveringMarker?.isPoly()) { - this.trigger(EVENTS.LEAVE_MARKER, this.prop.hoveringMarker); - - if (!this.prop.hoveringMarker.props.staticTooltip) { - this.prop.hoveringMarker.hideTooltip(); - } - - this.prop.hoveringMarker = null; - } - } - - /** - * @summary Handles mouse click events, select the marker and open the panel if necessary - * @param {Event} e - * @param {Object} data - * @param {boolean} dblclick - * @fires PSV.plugins.MarkersPlugin.select-marker - * @fires PSV.plugins.MarkersPlugin.unselect-marker - * @private - */ - __onClick(e, data, dblclick) { - let marker = data.objects.find(o => o.userData[MARKER_DATA])?.userData[MARKER_DATA]; - - if (!marker) { - marker = this.__getTargetMarker(data.target, true); - } - - if (this.prop.currentMarker && this.prop.currentMarker !== marker) { - this.trigger(EVENTS.UNSELECT_MARKER, this.prop.currentMarker); - - this.psv.panel.hide(ID_PANEL_MARKER); - - if (!this.prop.showAllTooltips && this.prop.currentMarker.config.tooltip.trigger === MARKER_TOOLTIP_TRIGGER.click) { - this.hideMarkerTooltip(this.prop.currentMarker); - } - - this.prop.currentMarker = null; - } - - if (marker) { - this.prop.currentMarker = marker; - - this.trigger(EVENTS.SELECT_MARKER, marker, { - dblclick : dblclick, - rightclick: data.rightclick, - }); - - if (this.config.clickEventOnMarker) { - // add the marker to event data - data.marker = marker; - } - else { - e.stopPropagation(); - } - - // the marker could have been deleted in an event handler - if (this.markers[marker.id]) { - if (marker.config.tooltip.trigger === MARKER_TOOLTIP_TRIGGER.click) { - if (marker.tooltip) { - this.hideMarkerTooltip(marker); - } - else { - this.showMarkerTooltip(marker); - } - } - else { - this.showMarkerPanel(marker.id); - } - } - } - } - - /** - * @summary Updates the visiblity of the panel and the buttons - * @private - */ - __refreshUi() { - const nbMarkers = Object.values(this.markers).filter(m => !m.config.hideList).length; - - if (nbMarkers === 0) { - if (this.psv.panel.isVisible(ID_PANEL_MARKERS_LIST)) { - this.psv.panel.hide(); - } - else if (this.psv.panel.isVisible(ID_PANEL_MARKER)) { - this.psv.panel.hide(); - } - } - else { - // eslint-disable-next-line no-lonely-if - if (this.psv.panel.isVisible(ID_PANEL_MARKERS_LIST)) { - this.showMarkersList(); - } - else if (this.psv.panel.isVisible(ID_PANEL_MARKER)) { - this.prop.currentMarker ? this.showMarkerPanel(this.prop.currentMarker) : this.psv.panel.hide(); - } - } - - this.psv.navbar.getButton(MarkersButton.id, false)?.toggle(nbMarkers > 0); - this.psv.navbar.getButton(MarkersListButton.id, false)?.toggle(nbMarkers > 0); - } - - /** - * @summary Adds or remove the objects observer if there are 3D markers - * @private - */ - __checkObjectsObserver() { - const has3d = Object.values(this.markers).some(marker => marker.is3d()); - - if (!has3d && this.prop.stopObserver) { - this.prop.stopObserver(); - this.prop.stopObserver = null; - } - else if (has3d && !this.prop.stopObserver) { - this.prop.stopObserver = this.psv.observeObjects(MARKER_DATA, this); - } - } - -} diff --git a/src/plugins/markers/style.scss b/src/plugins/markers/style.scss deleted file mode 100644 index 98c0f155a..000000000 --- a/src/plugins/markers/style.scss +++ /dev/null @@ -1,45 +0,0 @@ -@import '../../styles/vars'; - -.psv-markers { - user-select: none; - position: absolute; - z-index: $psv-hud-zindex; - width: 100%; - height: 100%; - - &-svg-container { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: $psv-polygon-marker-zindex; - } -} - -.psv-marker { - display: none; - - &--normal { - position: absolute; - top: 0; - left: 0; - z-index: $psv-marker-zindex; - background-size: contain; - background-repeat: no-repeat; - } - - &--transparent { - display: block; - opacity: 0; - } - - &--visible { - display: block; - } - - &--has-tooltip, - &--has-content { - cursor: pointer; - } -} diff --git a/src/plugins/markers/utils.js b/src/plugins/markers/utils.js deleted file mode 100644 index bbeedfaf2..000000000 --- a/src/plugins/markers/utils.js +++ /dev/null @@ -1,89 +0,0 @@ -import { CONSTANTS, utils } from '../..'; - -/** - * Returns intermediary point between two points on the sphere - * {@link http://www.movable-type.co.uk/scripts/latlong.html} - * @param {number[]} p1 - * @param {number[]} p2 - * @param {number} f - * @returns {number[]} - * @private - */ -export function greatArcIntermediaryPoint(p1, p2, f) { - const [λ1, φ1] = p1; - const [λ2, φ2] = p2; - - const r = utils.greatArcDistance(p1, p2); - const a = Math.sin((1 - f) * r) / Math.sin(r); - const b = Math.sin(f * r) / Math.sin(r); - const x = a * Math.cos(φ1) * Math.cos(λ1) + b * Math.cos(φ2) * Math.cos(λ2); - const y = a * Math.cos(φ1) * Math.sin(λ1) + b * Math.cos(φ2) * Math.sin(λ2); - const z = a * Math.sin(φ1) + b * Math.sin(φ2); - - return [ - Math.atan2(y, x), - Math.atan2(z, Math.sqrt(x * x + y * y)), - ]; -} - -/** - * @summary Computes the center point of a polygon - * @todo Get "visual center" (https://blog.mapbox.com/a-new-algorithm-for-finding-a-visual-center-of-a-polygon-7c77e6492fbc) - * @param {number[][]} polygon - * @returns {number[]} - * @private - */ -export function getPolygonCenter(polygon) { - // apply offsets to avoid crossing the origin - const workPoints = [polygon[0]]; - - let k = 0; - for (let i = 1; i < polygon.length; i++) { - const d = polygon[i - 1][0] - polygon[i][0]; - if (d > Math.PI) { // crossed the origin left to right - k += 1; - } - else if (d < -Math.PI) { // crossed the origin right to left - k -= 1; - } - workPoints.push([polygon[i][0] + k * 2 * Math.PI, polygon[i][1]]); - } - - const sum = workPoints.reduce((intermediary, point) => [intermediary[0] + point[0], intermediary[1] + point[1]]); - return [utils.parseAngle(sum[0] / polygon.length), sum[1] / polygon.length]; -} - -/** - * @summary Computes the middle point of a polyline - * @param {number[][]} polyline - * @returns {number[]} - * @private - */ -export function getPolylineCenter(polyline) { - // compute each segment length + total length - let length = 0; - const lengths = []; - - for (let i = 0; i < polyline.length - 1; i++) { - const l = utils.greatArcDistance(polyline[i], polyline[i + 1]) * CONSTANTS.SPHERE_RADIUS; - - lengths.push(l); - length += l; - } - - // iterate until length / 2 - let consumed = 0; - - for (let j = 0; j < polyline.length - 1; j++) { - // once the segment containing the middle point is found, computes the intermediary point - if (consumed + lengths[j] > length / 2) { - const r = (length / 2 - consumed) / lengths[j]; - return greatArcIntermediaryPoint(polyline[j], polyline[j + 1], r); - } - - consumed += lengths[j]; - } - - // this never happens - return polyline[Math.round(polyline.length / 2)]; -} diff --git a/src/plugins/resolution/constants.js b/src/plugins/resolution/constants.js deleted file mode 100644 index 860b0ee32..000000000 --- a/src/plugins/resolution/constants.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * @summary Available events - * @enum {string} - * @memberof PSV.plugins.ResolutionPlugin - * @constant - */ -export const EVENTS = { - /** - * @event resolution-changed - * @memberof PSV.plugins.ResolutionPlugin - * @summary Triggered when the resolution is changed - * @param {string} resolutionId - */ - RESOLUTION_CHANGED: 'resolution-changed', -}; diff --git a/src/plugins/resolution/index.js b/src/plugins/resolution/index.js deleted file mode 100644 index 63aee1e3a..000000000 --- a/src/plugins/resolution/index.js +++ /dev/null @@ -1,235 +0,0 @@ -import { AbstractPlugin, CONSTANTS, DEFAULTS, PSVError, utils } from '../..'; -import { EVENTS } from './constants'; - - -/** - * @typedef {Object} PSV.plugins.ResolutionPlugin.Resolution - * @property {string} id - * @property {string} label - * @property {*} panorama - */ - -/** - * @typedef {Object} PSV.plugins.ResolutionPlugin.Options - * @property {PSV.plugins.ResolutionPlugin.Resolution[]} resolutions - list of available resolutions - * @property {string} [defaultResolution] - the default resolution if no panorama is configured on the viewer - * @property {boolean} [showBadge=true] - show the resolution id as a badge on the settings button - */ - - -DEFAULTS.lang.resolution = 'Quality'; - - -export { EVENTS } from './constants'; - - -/** - * @summary Adds a setting to choose between multiple resolutions of the panorama. - * @extends PSV.plugins.AbstractPlugin - * @memberof PSV.plugins - */ -export class ResolutionPlugin extends AbstractPlugin { - - static id = 'resolution'; - - static EVENTS = EVENTS; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.plugins.ResolutionPlugin.Options} options - */ - constructor(psv, options) { - super(psv); - - /** - * @type {PSV.plugins.SettingsPlugin} - * @readonly - * @private - */ - this.settings = null; - - /** - * @summary Available resolutions - * @member {PSV.plugins.ResolutionPlugin.Resolution[]} - */ - this.resolutions = []; - - /** - * @summary Available resolutions - * @member {Object.} - * @private - */ - this.resolutionsById = {}; - - /** - * @type {Object} - * @property {string} resolution - Current resolution - * @private - */ - this.prop = { - resolution: null, - }; - - /** - * @type {PSV.plugins.ResolutionPlugin.Options} - */ - this.config = { - showBadge: true, - ...options, - }; - - if (this.config.defaultResolution && this.psv.config.panorama) { - utils.logWarn('ResolutionPlugin, a defaultResolution was provided ' - + 'but a panorama is already configured on the viewer, ' - + 'the defaultResolution will be ignored.'); - } - } - - /** - * @package - */ - init() { - super.init(); - - this.settings = this.psv.getPlugin('settings'); - - if (!this.settings) { - throw new PSVError('Resolution plugin requires the Settings plugin'); - } - - this.settings.addSetting({ - id : ResolutionPlugin.id, - type : 'options', - label : this.psv.config.lang.resolution, - current: () => this.prop.resolution, - options: () => this.__getSettingsOptions(), - apply : resolution => this.__setResolutionIfExists(resolution), - badge : !this.config.showBadge ? null : () => this.prop.resolution, - }); - - this.psv.on(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - - if (this.config.resolutions) { - this.setResolutions(this.config.resolutions, this.psv.config.panorama ? null : this.config.defaultResolution); - delete this.config.resolutions; - delete this.config.defaultResolution; - } - } - - /** - * @package - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - - this.settings.removeSetting(ResolutionPlugin.id); - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - if (e.type === CONSTANTS.EVENTS.PANORAMA_LOADED) { - this.__refreshResolution(); - } - } - - /** - * @summary Changes the available resolutions - * @param {PSV.plugins.ResolutionPlugin.Resolution[]} resolutions - * @param {string} [defaultResolution] - if not provided, the current panorama is kept - */ - setResolutions(resolutions, defaultResolution) { - this.resolutions = resolutions; - this.resolutionsById = {}; - - resolutions.forEach((resolution) => { - if (!resolution.id) { - throw new PSVError('Missing resolution id'); - } - this.resolutionsById[resolution.id] = resolution; - }); - - // pick first resolution if no default provided and no current panorama - if (!this.psv.config.panorama && !defaultResolution) { - defaultResolution = resolutions[0].id; - } - - // ensure the default resolution exists - if (defaultResolution && !this.resolutionsById[defaultResolution]) { - utils.logWarn(`Resolution ${defaultResolution} unknown`); - defaultResolution = resolutions[0].id; - } - - if (defaultResolution) { - this.setResolution(defaultResolution); - } - - this.__refreshResolution(); - } - - /** - * @summary Changes the current resolution - * @param {string} id - * @throws {PSVError} if the resolution does not exist - */ - setResolution(id) { - if (!this.resolutionsById[id]) { - throw new PSVError(`Resolution ${id} unknown`); - } - - return this.__setResolutionIfExists(id); - } - - /** - * @private - * @return {Promise} - */ - __setResolutionIfExists(id) { - if (this.resolutionsById[id]) { - return this.psv.setPanorama(this.resolutionsById[id].panorama, { transition: false, showLoader: false }); - } - else { - return Promise.resolve(); - } - } - - /** - * @summary Returns the current resolution - * @return {string} - */ - getResolution() { - return this.prop.resolution; - } - - /** - * @summary Updates current resolution on panorama load - * @private - */ - __refreshResolution() { - const resolution = this.resolutions.find(r => utils.deepEqual(this.psv.config.panorama, r.panorama)); - if (this.prop.resolution !== resolution?.id) { - this.prop.resolution = resolution?.id; - this.settings?.updateButton(); - this.trigger(EVENTS.RESOLUTION_CHANGED, this.prop.resolution); - } - } - - /** - * @summary Returns options for Settings plugin - * @return {PSV.plugins.SettingsPlugin.Option[]} - * @private - */ - __getSettingsOptions() { - return this.resolutions - .map(resolution => ({ - id : resolution.id, - label: resolution.label, - })); - } - -} diff --git a/src/plugins/settings/SettingsButton.js b/src/plugins/settings/SettingsButton.js deleted file mode 100644 index 411d4a36f..000000000 --- a/src/plugins/settings/SettingsButton.js +++ /dev/null @@ -1,71 +0,0 @@ -import { AbstractButton } from '../..'; -import icon from './settings.svg'; - -/** - * @summary Navigation bar settings button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class SettingsButton extends AbstractButton { - - static id = 'settings'; - static icon = icon; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-settings-button', true); - - /** - * @type {PSV.plugins.SettingsPlugin} - * @private - * @readonly - */ - this.plugin = this.psv.getPlugin('settings'); - - /** - * @member {HTMLElement} - * @private - * @readonly - */ - this.badge = document.createElement('div'); - this.badge.className = 'psv-settings-badge'; - this.badge.style.display = 'none'; - this.container.appendChild(this.badge); - } - - /** - * @override - */ - destroy() { - delete this.plugin; - - super.destroy(); - } - - /** - * @override - */ - isSupported() { - return !!this.plugin; - } - - /** - * @override - * @description Toggles settings - */ - onClick() { - this.plugin.toggleSettings(); - } - - /** - * @summary Changes the badge value - * @param {string} value - */ - setBadge(value) { - this.badge.innerText = value; - this.badge.style.display = value ? '' : 'none'; - } - -} diff --git a/src/plugins/settings/SettingsComponent.js b/src/plugins/settings/SettingsComponent.js deleted file mode 100644 index 2a9cc0a15..000000000 --- a/src/plugins/settings/SettingsComponent.js +++ /dev/null @@ -1,207 +0,0 @@ -import { AbstractComponent, utils } from '../..'; -import { EVENTS, KEY_CODES } from '../../data/constants'; -import { - ID_BACK, - ID_ENTER, - OPTION_DATA, - SETTING_DATA, - SETTING_OPTIONS_TEMPLATE, - SETTINGS_TEMPLATE, - TYPE_OPTIONS, - TYPE_TOGGLE -} from './constants'; - -/** - * @private - */ -export class SettingsComponent extends AbstractComponent { - - constructor(plugin) { - super(plugin.psv, 'psv-settings psv--capture-event'); - - /** - * @type {PSV.plugins.SettingsPlugin} - * @private - * @readonly - */ - this.plugin = plugin; - - /** - * @type {Object} - * @private - */ - this.prop = { - ...this.prop, - }; - - this.container.addEventListener('click', this); - this.container.addEventListener('transitionend', this); - this.container.addEventListener('keydown', this); - - this.hide(); - } - - /** - * @override - */ - destroy() { - delete this.plugin; - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case 'click': - this.__click(e.target); - break; - - case 'transitionend': - if (!this.isVisible()) { - this.container.innerHTML = ''; // empty content after fade out - } - else { - this.__focusFirstOption(); - } - break; - - case 'keydown': - if (this.isVisible()) { - switch (e.key) { - case KEY_CODES.Escape: - this.plugin.hideSettings(); - break; - case KEY_CODES.Enter: - this.__click(e.target); - break; - } - } - break; - - case EVENTS.KEY_PRESS: - if (this.isVisible() && e.args[0] === KEY_CODES.Escape) { - this.plugin.hideSettings(); - e.preventDefault(); - } - break; - } - /* eslint-enable */ - } - - /** - * @override - */ - show() { - this.__showSettings(false); - - this.container.classList.add('psv-settings--open'); - this.prop.visible = true; - } - - /** - * @override - */ - hide() { - this.container.classList.remove('psv-settings--open'); - this.prop.visible = false; - } - - /** - * @summary Handle clicks on items - * @param {HTMLElement} element - * @private - */ - __click(element) { - const li = utils.getClosest(element, 'li'); - if (!li) { - return; - } - - const settingId = li.dataset[SETTING_DATA]; - const optionId = li.dataset[OPTION_DATA]; - - const setting = this.plugin.settings.find(s => s.id === settingId); - - switch (optionId) { - case ID_BACK: - this.__showSettings(true); - break; - - case ID_ENTER: - switch (setting.type) { - case TYPE_TOGGLE: - this.plugin.toggleSettingValue(setting); - this.__showSettings(true); // re-render - break; - - case TYPE_OPTIONS: - this.__showOptions(setting); - break; - - default: - // noop - } - break; - - default: - switch (setting.type) { - case TYPE_OPTIONS: - this.hide(); - this.plugin.applySettingOption(setting, optionId); - break; - - default: - // noop - } - break; - } - } - - /** - * @summary Shows the list of options - * @private - */ - __showSettings(focus) { - this.container.innerHTML = SETTINGS_TEMPLATE( - this.plugin.settings, - (setting) => { - const current = setting.current(); - const option = setting.options() - .find(opt => opt.id === current); - return option?.label; - } - ); - - // must not focus during the initial transition - if (focus) { - this.__focusFirstOption(); - } - } - - /** - * @summary Shows setting options panel - * @param {PSV.plugins.SettingsPlugin.OptionsSetting} setting - * @private - */ - __showOptions(setting) { - const current = setting.current(); - - this.container.innerHTML = SETTING_OPTIONS_TEMPLATE( - setting, - (option) => { - return option.id === current; - } - ); - - this.__focusFirstOption(); - } - - __focusFirstOption() { - this.container.querySelector('[tabindex]')?.focus(); - } - -} diff --git a/src/plugins/settings/constants.js b/src/plugins/settings/constants.js deleted file mode 100644 index 5faa31ca8..000000000 --- a/src/plugins/settings/constants.js +++ /dev/null @@ -1,148 +0,0 @@ -import { utils } from '../..'; -import check from './check.svg'; -import chevron from './chevron.svg'; -import switchOff from './switch-off.svg'; -import switchOn from './switch-on.svg'; - -/** - * @summary Available events - * @enum {string} - * @memberof PSV.plugins.SettingsPlugin - * @constant - */ -export const EVENTS = { - /** - * @event setting-changed - * @memberof PSV.plugins.SettingsPlugin - * @summary Triggered when a setting is changed - * @param {string} settingId - * @param {any} value - */ - SETTING_CHANGED: 'setting-changed', -}; - -/** - * @type {string} - * @memberof PSV.plugins.SettingsPlugin - * @constant - */ -export const TYPE_OPTIONS = 'options'; - -/** - * @type {string} - * @memberof PSV.plugins.SettingsPlugin - * @constant - */ -export const TYPE_TOGGLE = 'toggle'; - -/** - * @summary Key of settings in LocalStorage - * @type {string} - * @constant - * @private - */ -export const LOCAL_STORAGE_KEY = 'psvSettings'; - -/** - * @summary Panel identifier for settings content - * @type {string} - * @constant - * @private - */ -export const ID_PANEL = 'settings'; - -/** - * @summary Property name added to settings items - * @type {string} - * @constant - * @private - */ -export const SETTING_DATA = 'settingId'; - -/** - * @summary Property name added to settings items - * @type {string} - * @constant - * @private - */ -export const OPTION_DATA = 'optionId'; - -/** - * @summary Identifier of the "back" list item - * @type {string} - * @constant - * @private - */ -export const ID_BACK = '__back'; - -/** - * @summary Identifier of the "back" list item - * @type {string} - * @constant - * @private - */ -export const ID_ENTER = '__enter'; - -const SETTING_DATA_KEY = utils.dasherize(SETTING_DATA); -const OPTION_DATA_KEY = utils.dasherize(OPTION_DATA); - -/** - * @summary Setting item template, by type - * @constant - * @private - */ -export const SETTINGS_TEMPLATE_ = { - [TYPE_OPTIONS]: (setting, optionsCurrent) => ` - ${setting.label} - ${optionsCurrent(setting)} - ${chevron} - `, - [TYPE_TOGGLE] : setting => ` - ${setting.label} - ${setting.active() ? switchOn : switchOff} - `, -}; - -/** - * @summary Settings list template - * @param {PSV.plugins.SettingsPlugin.Setting[]} settings - * @param {function} optionsCurrent - * @returns {string} - * @constant - * @private - */ -export const SETTINGS_TEMPLATE = (settings, optionsCurrent) => ` -
    - ${settings.map(s => ` -
  • - ${SETTINGS_TEMPLATE_[s.type](s, optionsCurrent)} -
  • - `).join('')} -
-`; - -/** - * @summary Settings options template - * @param {PSV.plugins.SettingsPlugin.OptionsSetting} setting - * @param {function} optionActive - * @returns {string} - * @constant - * @private - */ -export const SETTING_OPTIONS_TEMPLATE = (setting, optionActive) => ` -
    -
  • - ${chevron} - ${setting.label} -
  • - ${setting.options().map(option => ` -
  • - ${optionActive(option) ? check : ''} - ${option.label} -
  • - `).join('')} -
-`; diff --git a/src/plugins/settings/index.js b/src/plugins/settings/index.js deleted file mode 100644 index debadd1b2..000000000 --- a/src/plugins/settings/index.js +++ /dev/null @@ -1,319 +0,0 @@ -import { AbstractPlugin, CONSTANTS, DEFAULTS, PSVError, registerButton, utils } from '../..'; -import { EVENTS, LOCAL_STORAGE_KEY, SETTINGS_TEMPLATE_, TYPE_OPTIONS, TYPE_TOGGLE } from './constants'; -import { SettingsButton } from './SettingsButton'; -import { SettingsComponent } from './SettingsComponent'; -import './style.scss'; - - -/** - * @typedef {Object} PSV.plugins.SettingsPlugin.Setting - * @summary Description of a setting - * @property {string} id - identifier of the setting - * @property {string} label - label of the setting - * @property {'options' | 'toggle'} type - type of the setting - * @property {function} [badge] - function which returns the value of the button badge - */ - -/** - * @typedef {PSV.plugins.SettingsPlugin.Setting} PSV.plugins.SettingsPlugin.OptionsSetting - * @summary Description of a 'options' setting - * @property {'options'} type - type of the setting - * @property {function} current - function which returns the current option id - * @property {function} options - function which the possible options as an array of {@link PSV.plugins.SettingsPlugin.Option} - * @property {function} apply - function called with the id of the selected option - */ - -/** - * @typedef {PSV.plugins.SettingsPlugin.Setting} PSV.plugins.SettingsPlugin.ToggleSetting - * @summary Description of a 'toggle' setting - * @property {'toggle'} type - type of the setting - * @property {function} active - function which return whereas the setting is active or not - * @property {function} toggle - function called when the setting is toggled - */ - -/** - * @typedef {Object} PSV.plugins.SettingsPlugin.Option - * @summary Option of an 'option' setting - * @property {string} id - identifier of the option - * @property {string} label - label of the option - */ - -/** - * @typedef {Object} PSV.plugins.SettingsPlugin.Options - * @property {boolean} [persist=false] - should the settings be saved accross sessions - * @property {Object} [storage] - custom storage handler, defaults to LocalStorage - * @property {PSV.plugins.SettingsPlugin.StorageGetter} [storage.get] - * @property {PSV.plugins.SettingsPlugin.StorageSetter} [storage.set] - */ - -/** - * @callback StorageGetter - * @memberOf PSV.plugins.SettingsPlugin - * @param {string} settingId - * @return {boolean | string | Promise} - return `undefined` or `null` if the option does not exist - */ - -/** - * @callback StorageSetter - * @memberOf PSV.plugins.SettingsPlugin - * @param {string} settingId - * @param {boolean | string} value - */ - - -// add settings button -DEFAULTS.lang[SettingsButton.id] = 'Settings'; -registerButton(SettingsButton, 'fullscreen:left'); - - -function getData() { - return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY)) || {}; -} - -function setData(data) { - localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(data)); -} - - -export { EVENTS, TYPE_TOGGLE, TYPE_OPTIONS } from './constants'; - - -/** - * @summary Adds a button to access various settings. - * @extends PSV.plugins.AbstractPlugin - * @memberof PSV.plugins - */ -export class SettingsPlugin extends AbstractPlugin { - - static id = 'settings'; - - static EVENTS = EVENTS; - static TYPE_TOGGLE = TYPE_TOGGLE; - static TYPE_OPTIONS = TYPE_OPTIONS; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.plugins.SettingsPlugin.Options} options - */ - constructor(psv, options) { - super(psv); - - /** - * @type {PSV.plugins.SettingsPlugin.Options} - */ - this.config = { - persist: false, - storage: { - get(id) { - return getData()[id]; - }, - set(id, value) { - const data = getData(); - data[id] = value; - setData(data); - }, - }, - ...options, - }; - - /** - * @type {SettingsComponent} - * @private - * @readonly - */ - this.component = new SettingsComponent(this); - - /** - * @type {PSV.plugins.SettingsPlugin.Setting[]} - * @private - */ - this.settings = []; - } - - /** - * @package - */ - init() { - super.init(); - - this.psv.on(CONSTANTS.EVENTS.CLICK, this); - this.psv.on(CONSTANTS.EVENTS.OPEN_PANEL, this); - - // buttons are initialized just after plugins - setTimeout(() => this.updateButton()); - } - - /** - * @package - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.CLICK, this); - this.psv.off(CONSTANTS.EVENTS.OPEN_PANEL, this); - - this.component.destroy(); - - delete this.component; - this.settings.length = 0; - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case CONSTANTS.EVENTS.CLICK: - case CONSTANTS.EVENTS.OPEN_PANEL: - if (this.component.isVisible()) { - this.hideSettings(); - } - break; - } - /* eslint-enable */ - } - - /** - * @summary Registers a new setting - * @param {PSV.plugins.SettingsPlugin.Setting} setting - */ - addSetting(setting) { - if (!setting.id) { - throw new PSVError('Missing setting id'); - } - if (!setting.type) { - throw new PSVError('Missing setting type'); - } - if (!SETTINGS_TEMPLATE_[setting.type]) { - throw new PSVError('Unsupported setting type'); - } - - if (setting.badge && this.settings.some(s => s.badge)) { - utils.logWarn('More than one setting with a badge are declared, the result is unpredictable.'); - } - - this.settings.push(setting); - - if (this.component.isVisible()) { - this.component.show(); // re-render - } - - this.updateButton(); - - if (this.config.persist) { - Promise.resolve(this.config.storage.get(setting.id)) - .then((value) => { - switch (setting.type) { - case TYPE_TOGGLE: - if (!utils.isNil(value) && value !== setting.active()) { - setting.toggle(); - this.trigger(EVENTS.SETTING_CHANGED, setting.id, setting.active()); - } - break; - - case TYPE_OPTIONS: - if (!utils.isNil(value) && value !== setting.current()) { - setting.apply(value); - this.trigger(EVENTS.SETTING_CHANGED, setting.id, setting.current()); - } - break; - - default: - // noop - } - - this.updateButton(); - }); - } - } - - /** - * @summary Removes a setting - * @param {string} id - */ - removeSetting(id) { - const idx = this.settings.findIndex(setting => setting.id === id); - if (idx !== -1) { - this.settings.splice(idx, 1); - - if (this.component.isVisible()) { - this.component.show(); // re-render - } - - this.updateButton(); - } - } - - /** - * @summary Toggles the settings menu - */ - toggleSettings() { - this.component.toggle(); - this.updateButton(); - } - - /** - * @summary Hides the settings menu - */ - hideSettings() { - this.component.hide(); - this.updateButton(); - } - - /** - * @summary Shows the settings menu - */ - showSettings() { - this.component.show(); - this.updateButton(); - } - - /** - * @summary Updates the badge in the button - */ - updateButton() { - const value = this.settings.find(s => s.badge)?.badge(); - const button = this.psv.navbar.getButton(SettingsButton.id, false); - button?.toggleActive(this.component.isVisible()); - button?.setBadge(value); - } - - /** - * @summary Toggles a setting - * @param {PSV.plugins.SettingsPlugin.ToggleSetting} setting - * @package - */ - toggleSettingValue(setting) { - const newValue = !setting.active(); // in case "toggle" is async - - setting.toggle(); - - this.trigger(EVENTS.SETTING_CHANGED, setting.id, newValue); - - if (this.config.persist) { - this.config.storage.set(setting.id, newValue); - } - - this.updateButton(); - } - - /** - * @summary Changes the value of an setting - * @param {PSV.plugins.SettingsPlugin.OptionsSetting} setting - * @param {string} optionId - * @package - */ - applySettingOption(setting, optionId) { - setting.apply(optionId); - - this.trigger(EVENTS.SETTING_CHANGED, setting.id, optionId); - - if (this.config.persist) { - this.config.storage.set(setting.id, optionId); - } - - this.updateButton(); - } - -} diff --git a/src/plugins/settings/style.scss b/src/plugins/settings/style.scss deleted file mode 100644 index f9e060acd..000000000 --- a/src/plugins/settings/style.scss +++ /dev/null @@ -1,96 +0,0 @@ -@use 'sass:list'; -@import '../../styles/vars'; - -$psv-settings-margin: 10px !default; -$psv-settings-item-height: $psv-panel-menu-item-height !default; -$psv-settings-item-padding: $psv-panel-menu-item-padding !default; -$psv-settings-background: $psv-panel-background !default; -$psv-settings-shadow: 0 0 5px $psv-settings-background !default; -$psv-settings-text-color: $psv-panel-text-color !default; -$psv-settings-hover-background: $psv-panel-menu-hover-background !default; -$psv-settings-badge-font: 10px / .9 monospace !default; -$psv-settings-badge-background: #111 !default; -$psv-settings-badge-text-color: white !default; - -.psv-settings { - position: absolute; - bottom: $psv-navbar-height + $psv-settings-margin; - right: $psv-settings-margin; - background: $psv-settings-background; - box-shadow: $psv-settings-shadow; - color: $psv-settings-text-color; - z-index: $psv-navbar-zindex; - opacity: 0; - transition: opacity .1s linear; - - &--open { - opacity: 1; - } - - &-list { - list-style: none; - margin: 0; - padding: 0; - } -} - -.psv-settings-item { - height: $psv-settings-item-height; - padding: $psv-settings-item-padding; - display: flex; - align-items: center; - justify-content: flex-start; - cursor: pointer; - - &:hover { - background: $psv-settings-hover-background; - } - - &:focus-visible { - outline: $psv-element-focus-outline; - outline-offset: -#{list.nth($psv-element-focus-outline, 1)}; - } - - *:not(:last-child) { - margin-right: 1em; - } - - &-label { - flex: 1; - font-weight: bold; - } - - &-value { - flex: none; - } - - &-icon { - flex: none; - height: 1em; - width: 1em; - - svg { - width: 1em; - height: 1em; - } - } - - &--header { - border-bottom: 1px solid currentcolor; - - svg { - transform: scaleX(-1); - } - } -} - -.psv-settings-badge { - position: absolute; - top: 10%; - right: 10%; - border-radius: .2em; - padding: .2em; - background: $psv-settings-badge-background; - color: $psv-settings-badge-text-color; - font: $psv-settings-badge-font; -} diff --git a/src/plugins/shared/autorotate-utils.js b/src/plugins/shared/autorotate-utils.js deleted file mode 100644 index b288042d7..000000000 --- a/src/plugins/shared/autorotate-utils.js +++ /dev/null @@ -1,38 +0,0 @@ -let debugMarkers = []; - -/** - * @private - */ -export function debugCurve(markers, curve, stepSize) { - debugMarkers.forEach((marker) => { - try { - markers.removeMarker(marker.id); - } - catch (e) { - // noop - } - }); - - debugMarkers = [ - markers.addMarker({ - id : 'autorotate-path', - polylineRad: curve, - svgStyle : { - stroke : 'white', - strokeWidth: '2px', - }, - }), - ]; - - curve.forEach((pos, i) => { - debugMarkers.push(markers.addMarker({ - id : 'autorotate-path-' + i, - circle : 5, - longitude: pos[0], - latitude : pos[1], - svgStyle : { - fill: i % stepSize === 0 ? 'red' : 'black', - }, - })); - }); -} diff --git a/src/plugins/stereo/StereoButton.js b/src/plugins/stereo/StereoButton.js deleted file mode 100644 index be839c0d2..000000000 --- a/src/plugins/stereo/StereoButton.js +++ /dev/null @@ -1,72 +0,0 @@ -import { AbstractButton } from '../..'; -import { EVENTS } from './constants'; -import stereo from './stereo.svg'; - -/** - * @summary Navigation bar stereo button class - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class StereoButton extends AbstractButton { - - static id = 'stereo'; - static icon = stereo; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-stereo-button', true); - - /** - * @type {PSV.plugins.StereoPlugin} - * @private - * @readonly - */ - this.plugin = this.psv.getPlugin('stereo'); - - if (this.plugin) { - this.plugin.on(EVENTS.STEREO_UPDATED, this); - } - } - - /** - * @override - */ - destroy() { - if (this.plugin) { - this.plugin.off(EVENTS.STEREO_UPDATED, this); - } - - delete this.plugin; - - super.destroy(); - } - - /** - * @override - */ - isSupported() { - return !this.plugin ? false : { initial: false, promise: this.plugin.prop.isSupported }; - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - if (e.type === EVENTS.STEREO_UPDATED) { - this.toggleActive(e.args[0]); - } - } - - /** - * @override - * @description Toggles stereo control - */ - onClick() { - this.plugin.toggle(); - } - -} diff --git a/src/plugins/stereo/StereoEffect.js b/src/plugins/stereo/StereoEffect.js deleted file mode 100644 index 1318146aa..000000000 --- a/src/plugins/stereo/StereoEffect.js +++ /dev/null @@ -1,59 +0,0 @@ -import { - StereoCamera, - Vector2 -} from 'three'; - -/** - * Copied from three.js examples - * @private - */ -class StereoEffect { - - constructor( renderer ) { - - const _stereo = new StereoCamera(); - _stereo.aspect = 0.5; - const size = new Vector2(); - - this.setEyeSeparation = function ( eyeSep ) { - - _stereo.eyeSep = eyeSep; - - }; - - this.setSize = function ( width, height ) { - - renderer.setSize( width, height ); - - }; - - this.render = function ( scene, camera ) { - - scene.updateMatrixWorld(); - - if ( camera.parent === null ) camera.updateMatrixWorld(); - - _stereo.update( camera ); - - renderer.getSize( size ); - - if ( renderer.autoClear ) renderer.clear(); - renderer.setScissorTest( true ); - - renderer.setScissor( 0, 0, size.width / 2, size.height ); - renderer.setViewport( 0, 0, size.width / 2, size.height ); - renderer.render( scene, _stereo.cameraL ); - - renderer.setScissor( size.width / 2, 0, size.width / 2, size.height ); - renderer.setViewport( size.width / 2, 0, size.width / 2, size.height ); - renderer.render( scene, _stereo.cameraR ); - - renderer.setScissorTest( false ); - - }; - - } - -} - -export { StereoEffect }; diff --git a/src/plugins/stereo/constants.js b/src/plugins/stereo/constants.js deleted file mode 100644 index bf8105a17..000000000 --- a/src/plugins/stereo/constants.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @summary Available events - * @enum {string} - * @memberof PSV.plugins.StereoPlugin - * @constant - */ -export const EVENTS = { - /** - * @event stereo-updated - * @memberof PSV.plugins.StereoPlugin - * @summary Triggered when the stereo view is enabled/disabled - * @param {boolean} enabled - */ - STEREO_UPDATED: 'stereo-updated', -}; - -/** - * @type {string} - * @constant - * @private - */ -export const ID_OVERLAY_PLEASE_ROTATE = 'pleaseRotate'; diff --git a/src/plugins/stereo/index.js b/src/plugins/stereo/index.js deleted file mode 100644 index 541c11aa0..000000000 --- a/src/plugins/stereo/index.js +++ /dev/null @@ -1,299 +0,0 @@ -import { AbstractPlugin, CONSTANTS, DEFAULTS, PSVError, registerButton, utils } from '../..'; -import { EVENTS, ID_OVERLAY_PLEASE_ROTATE } from './constants'; -import mobileRotateIcon from './mobile-rotate.svg'; -import { StereoButton } from './StereoButton'; -import { StereoEffect } from './StereoEffect'; - - -/** - * @external NoSleep - * @description {@link https://github.com/richtr/NoSleep.js} - */ - - -// add stereo button -DEFAULTS.lang[StereoButton.id] = 'Stereo view'; -registerButton(StereoButton, 'caption:right'); - -// other lang strings -DEFAULTS.lang.stereoNotification = 'Tap anywhere to exit stereo view.'; -DEFAULTS.lang.pleaseRotate = ['Please rotate your device', '(or tap to continue)']; - - -export { EVENTS } from './constants'; - - -/** - * @summary Adds stereo view on mobile devices - * @extends PSV.plugins.AbstractPlugin - * @memberof PSV.plugins - */ -export class StereoPlugin extends AbstractPlugin { - - static id = 'stereo'; - - static EVENTS = EVENTS; - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv); - - /** - * @type {PSV.plugins.GyroscopePlugin} - * @readonly - * @private - */ - this.gyroscope = null; - - /** - * @type {PSV.plugins.MarkersPlugin} - * @readonly - * @private - */ - this.markers = null; - - /** - * @type {PSV.plugins.CompassPlugin} - * @readonly - * @private - */ - this.compass = null; - - /** - * @member {Object} - * @protected - * @property {Promise} isSupported - indicates of the gyroscope API is available - * @property {external:THREE.WebGLRenderer} renderer - original renderer - * @property {external:NoSleep} noSleep - * @property {WakeLockSentinel} wakeLock - */ - this.prop = { - isSupported: false, - renderer : null, - noSleep : null, - wakeLock : null, - }; - } - - /** - * @package - */ - init() { - super.init(); - - this.markers = this.psv.getPlugin('markers'); - this.compass = this.psv.getPlugin('compass'); - this.gyroscope = this.psv.getPlugin('gyroscope'); - - if (!this.gyroscope) { - throw new PSVError('Stereo plugin requires the Gyroscope plugin'); - } - - this.prop.isSupported = this.gyroscope.prop.isSupported; - - this.psv.on(CONSTANTS.EVENTS.STOP_ALL, this); - this.psv.on(CONSTANTS.EVENTS.CLICK, this); - } - - /** - * @package - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.STOP_ALL, this); - this.psv.off(CONSTANTS.EVENTS.CLICK, this); - - this.stop(); - - delete this.markers; - delete this.compass; - delete this.gyroscope; - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - switch (e.type) { - case CONSTANTS.EVENTS.STOP_ALL: - case CONSTANTS.EVENTS.CLICK: - this.stop(); - break; - default: - break; - } - } - - /** - * @summary Checks if the stereo view is enabled - * @returns {boolean} - */ - isEnabled() { - return !!this.prop.renderer; - } - - /** - * @summary Enables the stereo view - * @description - * - enables NoSleep.js - * - enables full screen - * - starts gyroscope controle - * - hides markers, navbar and panel - * - instanciate {@link external:THREE.StereoEffect} - * @returns {Promise} - * @fires PSV.plugins.StereoPlugin.stereo-updated - * @throws {PSV.PSVError} if the gyroscope API is not available/granted - */ - start() { - // Need to be in the main event queue - this.psv.enterFullscreen(); - this.__startNoSleep(); - this.__lockOrientation(); - - return this.gyroscope.start().then(() => { - // switch renderer - this.prop.renderer = this.psv.renderer.renderer; - this.psv.renderer.renderer = new StereoEffect(this.psv.renderer.renderer); - - this.psv.needsUpdate(); - - this.markers?.hide(); - this.compass?.hide(); - this.psv.navbar.hide(); - this.psv.panel.hide(); - - this.trigger(EVENTS.STEREO_UPDATED, true); - - this.psv.notification.show({ - content: this.psv.config.lang.stereoNotification, - timeout: 3000, - }); - }, () => { - this.__unlockOrientation(); - this.__stopNoSleep(); - this.psv.exitFullscreen(); - }); - } - - /** - * @summary Disables the stereo view - * @fires PSV.plugins.StereoPlugin.stereo-updated - */ - stop() { - if (this.isEnabled()) { - this.psv.renderer.renderer = this.prop.renderer; - this.prop.renderer = null; - - this.psv.needsUpdate(); - - this.markers?.show(); - this.compass?.show(); - this.psv.navbar.show(); - - this.__unlockOrientation(); - this.__stopNoSleep(); - this.psv.exitFullscreen(); - this.gyroscope.stop(); - - this.trigger(EVENTS.STEREO_UPDATED, false); - } - } - - /** - * @summary Enables or disables the stereo view - */ - toggle() { - if (this.isEnabled()) { - this.stop(); - } - else { - this.start(); - } - } - - /** - * @summary Enables WakeLock or NoSleep.js - * @private - */ - __startNoSleep() { - if ('wakeLock' in navigator) { - navigator.wakeLock.request('screen') - .then((wakeLock) => { - this.prop.wakeLock = wakeLock; - }) - .catch(() => utils.logWarn('Cannot acquire WakeLock')); - } - else if ('NoSleep' in window) { - if (!this.prop.noSleep) { - this.prop.noSleep = new window.NoSleep(); - } - this.prop.noSleep.enable(); - } - else { - utils.logWarn('NoSleep is not available'); - } - } - - /** - * @summary Disables WakeLock or NoSleep.js - * @private - */ - __stopNoSleep() { - if (this.prop.wakeLock) { - this.prop.wakeLock.release(); - this.prop.wakeLock = null; - } - else if (this.prop.noSleep) { - this.prop.noSleep.disable(); - } - } - - /** - * @summary Tries to lock the device in landscape or display a message - * @private - */ - __lockOrientation() { - let displayRotateMessageTimeout; - - const displayRotateMessage = () => { - if (window.innerHeight > window.innerWidth) { - this.psv.overlay.show({ - id : ID_OVERLAY_PLEASE_ROTATE, - image : mobileRotateIcon, - text : this.psv.config.lang.pleaseRotate[0], - subtext: this.psv.config.lang.pleaseRotate[1], - }); - } - - if (displayRotateMessageTimeout) { - clearTimeout(displayRotateMessageTimeout); - displayRotateMessageTimeout = null; - } - }; - - if (window.screen?.orientation) { - window.screen.orientation.lock('landscape').then(null, () => displayRotateMessage()); - displayRotateMessageTimeout = setTimeout(() => displayRotateMessage(), 500); - } - else { - displayRotateMessage(); - } - } - - /** - * @summary Unlock the device orientation - * @private - */ - __unlockOrientation() { - if (window.screen?.orientation) { - window.screen.orientation.unlock(); - } - else { - this.psv.overlay.hide(ID_OVERLAY_PLEASE_ROTATE); - } - } - -} diff --git a/src/plugins/video/PauseOverlay.js b/src/plugins/video/PauseOverlay.js deleted file mode 100644 index 7a37ac595..000000000 --- a/src/plugins/video/PauseOverlay.js +++ /dev/null @@ -1,69 +0,0 @@ -import { AbstractComponent, CONSTANTS, utils } from '../..'; -import { EVENTS } from './constants'; -import playIcon from './play.svg'; - -/** - * @private - */ -export class PauseOverlay extends AbstractComponent { - - constructor(plugin) { - super(plugin.psv, 'psv-video-overlay'); - - /** - * @type {PSV.plugins.VideoPlugin} - * @private - * @readonly - */ - this.plugin = plugin; - - /** - * @type {HTMLElement} - * @private - * @readonly - */ - this.button = document.createElement('button'); - this.button.className = 'psv-video-bigbutton psv--capture-event'; - this.button.innerHTML = playIcon; - this.container.appendChild(this.button); - - this.psv.on(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.plugin.on(EVENTS.PLAY, this); - this.plugin.on(EVENTS.PAUSE, this); - this.button.addEventListener('click', this); - } - - /** - * @private - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.plugin.off(EVENTS.PLAY, this); - this.plugin.off(EVENTS.PAUSE, this); - - delete this.plugin; - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case CONSTANTS.EVENTS.PANORAMA_LOADED: - case EVENTS.PLAY: - case EVENTS.PAUSE: - utils.toggleClass(this.button, 'psv-video-bigbutton--pause', !this.plugin.isPlaying()); - break; - case 'click': - this.plugin.playPause(); - break; - } - /* eslint-enable */ - } - -} diff --git a/src/plugins/video/PlayPauseButton.js b/src/plugins/video/PlayPauseButton.js deleted file mode 100644 index fc7b34f7f..000000000 --- a/src/plugins/video/PlayPauseButton.js +++ /dev/null @@ -1,82 +0,0 @@ -import { AbstractButton } from '../..'; -import { EVENTS } from './constants'; -import pauseIcon from './pause.svg'; -import playIcon from './play.svg'; - -/** - * @summary Navigation bar video play/pause button - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class PlayPauseButton extends AbstractButton { - - static id = 'videoPlay'; - static groupId = 'video'; - static icon = playIcon; - static iconActive = pauseIcon; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-video-play-button', true); - - /** - * @type {PSV.plugins.VideoPlugin} - * @private - * @readonly - */ - this.plugin = this.psv.getPlugin('video'); - - if (this.plugin) { - this.plugin.on(EVENTS.PLAY, this); - this.plugin.on(EVENTS.PAUSE, this); - } - } - - /** - * @override - */ - destroy() { - if (this.plugin) { - this.plugin.off(EVENTS.PLAY, this); - this.plugin.off(EVENTS.PAUSE, this); - } - - delete this.plugin; - - super.destroy(); - } - - /** - * @override - */ - isSupported() { - return !!this.plugin; - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case EVENTS.PLAY: - case EVENTS.PAUSE: - this.toggleActive(this.plugin.isPlaying()); - break; - } - /* eslint-enable */ - } - - /** - * @override - * @description Toggles video playback - */ - onClick() { - this.plugin.playPause(); - } - -} diff --git a/src/plugins/video/ProgressBar.js b/src/plugins/video/ProgressBar.js deleted file mode 100644 index 19e0c1bc1..000000000 --- a/src/plugins/video/ProgressBar.js +++ /dev/null @@ -1,147 +0,0 @@ -import { AbstractComponent, CONSTANTS, utils } from '../..'; -import { EVENTS } from './constants'; -import { formatTime } from './utils'; - -/** - * @private - */ -export class ProgressBar extends AbstractComponent { - - constructor(plugin) { - super(plugin.psv, 'psv-video-progressbar'); - - /** - * @type {PSV.plugins.VideoPlugin} - * @private - * @readonly - */ - this.plugin = plugin; - - /** - * @type {HTMLElement} - * @private - * @readonly - */ - this.bufferElt = document.createElement('div'); - this.bufferElt.className = 'psv-video-progressbar__buffer'; - this.container.appendChild(this.bufferElt); - - /** - * @type {HTMLElement} - * @private - * @readonly - */ - this.progressElt = document.createElement('div'); - this.progressElt.className = 'psv-video-progressbar__progress'; - this.container.appendChild(this.progressElt); - - /** - * @type {HTMLElement} - * @private - * @readonly - */ - this.handleElt = document.createElement('div'); - this.handleElt.className = 'psv-video-progressbar__handle'; - this.container.appendChild(this.handleElt); - - /** - * @type {PSV.utils.Slider} - * @private - * @readonly - */ - this.slider = new utils.Slider({ - psv : this.psv, - container: this.container, - direction: utils.Slider.HORIZONTAL, - onUpdate : e => this.__onSliderUpdate(e), - }); - - this.psv.on(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.plugin.on(EVENTS.BUFFER, this); - this.plugin.on(EVENTS.PROGRESS, this); - - this.prop.req = window.requestAnimationFrame(() => this.__updateProgress()); - - this.hide(); - } - - /** - * @override - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.plugin.off(EVENTS.BUFFER, this); - this.plugin.off(EVENTS.PROGRESS, this); - - this.slider.destroy(); - this.prop.tooltip?.hide(); - window.cancelAnimationFrame(this.prop.req); - - delete this.prop.tooltip; - delete this.slider; - delete this.plugin; - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case CONSTANTS.EVENTS.PANORAMA_LOADED: - case EVENTS.BUFFER: - case EVENTS.PROGRESS: - this.bufferElt.style.width = `${this.plugin.getBufferProgress() * 100}%`; - break; - } - /* eslint-enable */ - } - - /** - * @private - */ - __updateProgress() { - this.progressElt.style.width = `${this.plugin.getProgress() * 100}%`; - - this.prop.req = window.requestAnimationFrame(() => this.__updateProgress()); - } - - /** - * @private - */ - __onSliderUpdate(e) { - if (e.mouseover) { - this.handleElt.style.display = 'block'; - this.handleElt.style.left = `${e.value * 100}%`; - - const time = formatTime(this.plugin.getDuration() * e.value); - - if (!this.prop.tooltip) { - this.prop.tooltip = this.psv.tooltip.create({ - top : e.cursor.clientY, - left : e.cursor.clientX, - content: time, - }); - } - else { - this.prop.tooltip.content.innerHTML = time; - this.prop.tooltip.move({ - top : e.cursor.clientY, - left: e.cursor.clientX, - }); - } - } - else { - this.handleElt.style.display = 'none'; - - this.prop.tooltip?.hide(); - delete this.prop.tooltip; - } - if (e.click) { - this.plugin.setProgress(e.value); - } - } - -} diff --git a/src/plugins/video/TimeCaption.js b/src/plugins/video/TimeCaption.js deleted file mode 100644 index 8d0f09203..000000000 --- a/src/plugins/video/TimeCaption.js +++ /dev/null @@ -1,73 +0,0 @@ -import { AbstractComponent, CONSTANTS } from '../..'; -import { EVENTS } from './constants'; -import { formatTime } from './utils'; - -/** - * @summary Navigation bar video time display - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class TimeCaption extends AbstractComponent { - - static id = 'videoTime'; - static groupId = 'video'; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-caption psv-video-time'); - - /** - * @member {HTMLElement} - * @readonly - * @private - */ - this.content = document.createElement('div'); - this.content.className = 'psv-caption-content'; - this.container.appendChild(this.content); - - /** - * @type {PSV.plugins.VideoPlugin} - * @private - * @readonly - */ - this.plugin = this.psv.getPlugin('video'); - - if (this.plugin) { - this.psv.on(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.plugin.on(EVENTS.PROGRESS, this); - } - } - - /** - * @override - */ - destroy() { - if (this.plugin) { - this.psv.off(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.plugin.off(EVENTS.PROGRESS, this); - } - - delete this.plugin; - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case CONSTANTS.EVENTS.PANORAMA_LOADED: - case EVENTS.PROGRESS: - this.content.innerHTML = `${formatTime(this.plugin.getTime())} / ${formatTime(this.plugin.getDuration())}`; - break; - } - /* eslint-enable */ - } - -} diff --git a/src/plugins/video/VolumeButton.js b/src/plugins/video/VolumeButton.js deleted file mode 100644 index a12af9e88..000000000 --- a/src/plugins/video/VolumeButton.js +++ /dev/null @@ -1,172 +0,0 @@ -import { AbstractButton, CONSTANTS, utils } from '../..'; -import { EVENTS } from './constants'; -import volumeIcon from './volume.svg'; - -/** - * @summary Navigation bar video volume button - * @extends PSV.buttons.AbstractButton - * @memberof PSV.buttons - */ -export class VolumeButton extends AbstractButton { - - static id = 'videoVolume'; - static groupId = 'video'; - static icon = volumeIcon; - - /** - * @param {PSV.components.Navbar} navbar - */ - constructor(navbar) { - super(navbar, 'psv-button--hover-scale psv-video-volume-button', true); - - /** - * @type {PSV.plugins.VideoPlugin} - * @private - * @readonly - */ - this.plugin = this.psv.getPlugin('video'); - - /** - * @type {HTMLElement} - * @private - * @readonly - */ - this.rangeContainer = document.createElement('div'); - this.rangeContainer.className = 'psv-video-volume__container'; - this.container.appendChild(this.rangeContainer); - - /** - * @type {HTMLElement} - * @private - * @readonly - */ - this.range = document.createElement('div'); - this.range.className = 'psv-video-volume__range'; - this.rangeContainer.appendChild(this.range); - - /** - * @type {HTMLElement} - * @private - * @readonly - */ - this.trackElt = document.createElement('div'); - this.trackElt.className = 'psv-video-volume__track'; - this.range.appendChild(this.trackElt); - - /** - * @type {HTMLElement} - * @private - * @readonly - */ - this.progressElt = document.createElement('div'); - this.progressElt.className = 'psv-video-volume__progress'; - this.range.appendChild(this.progressElt); - - /** - * @type {HTMLElement} - * @private - * @readonly - */ - this.handleElt = document.createElement('div'); - this.handleElt.className = 'psv-video-volume__handle'; - this.range.appendChild(this.handleElt); - - /** - * @type {PSV.utils.Slider} - * @private - * @readonly - */ - this.slider = new utils.Slider({ - psv : this.psv, - container: this.range, - direction: utils.Slider.VERTICAL, - onUpdate : e => this.__onSliderUpdate(e), - }); - - if (this.plugin) { - this.psv.on(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.plugin.on(EVENTS.PLAY, this); - this.plugin.on(EVENTS.VOLUME_CHANGE, this); - - this.__setVolume(0); - } - } - - /** - * @override - */ - destroy() { - if (this.plugin) { - this.psv.off(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.plugin.off(EVENTS.PLAY, this); - this.plugin.off(EVENTS.VOLUME_CHANGE, this); - } - - this.slider.destroy(); - - delete this.plugin; - - super.destroy(); - } - - /** - * @override - */ - isSupported() { - return !!this.plugin; - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - case CONSTANTS.EVENTS.PANORAMA_LOADED: - case EVENTS.PLAY: - case EVENTS.VOLUME_CHANGE: - this.__setVolume(this.plugin.getVolume()); - break; - } - /* eslint-enable */ - } - - /** - * @override - * @description Toggles video muted - */ - onClick() { - this.plugin.setMute(); - } - - /** - * @private - */ - __onSliderUpdate(e) { - if (e.mousedown) { - this.plugin.setVolume(e.value); - } - } - - /** - * @private - */ - __setVolume(volume) { - let level; - if (volume === 0) level = 0; - else if (volume < 0.333) level = 1; - else if (volume < 0.666) level = 2; - else level = 3; - - utils.toggleClass(this.container, 'psv-video-volume-button--0', level === 0); - utils.toggleClass(this.container, 'psv-video-volume-button--1', level === 1); - utils.toggleClass(this.container, 'psv-video-volume-button--2', level === 2); - utils.toggleClass(this.container, 'psv-video-volume-button--3', level === 3); - - this.handleElt.style.bottom = `${volume * 100}%`; - this.progressElt.style.height = `${volume * 100}%`; - } - -} diff --git a/src/plugins/video/constants.js b/src/plugins/video/constants.js deleted file mode 100644 index b9021d012..000000000 --- a/src/plugins/video/constants.js +++ /dev/null @@ -1,35 +0,0 @@ -export const EVENTS = { - /** - * @event play - * @memberof PSV.plugins.VideoPlugin - * @summary Triggered when the video starts playing - */ - PLAY : 'play', - /** - * @event pause - * @memberof PSV.plugins.VideoPlugin - * @summary Triggered when the video is paused - */ - PAUSE : 'pause', - /** - * @event volume-change - * @memberof PSV.plugins.VideoPlugin - * @summary Triggered when the video volume changes - * @param {number} volume - */ - VOLUME_CHANGE: 'volume-change', - /** - * @event progress - * @memberof PSV.plugins.VideoPlugin - * @summary Triggered when the video play progression changes - * @param {{time: number, duration: number, progress: number}} data - */ - PROGRESS : 'progress', - /** - * @event buffer - * @memberof PSV.plugins.VideoPlugin - * @summary Triggered when the video buffer changes - * @param {number} maxBuffer - */ - BUFFER : 'buffer', -}; diff --git a/src/plugins/video/index.js b/src/plugins/video/index.js deleted file mode 100644 index ce5ff8e82..000000000 --- a/src/plugins/video/index.js +++ /dev/null @@ -1,471 +0,0 @@ -import { SplineCurve, Vector2 } from 'three'; -import { AbstractPlugin, CONSTANTS, DEFAULTS, PSVError, registerButton, utils } from '../..'; -import { EVENTS } from './constants'; -import { PauseOverlay } from './PauseOverlay'; -import { PlayPauseButton } from './PlayPauseButton'; -import { ProgressBar } from './ProgressBar'; -import { TimeCaption } from './TimeCaption'; -import { VolumeButton } from './VolumeButton'; -import './style.scss'; - - -/** - * @typedef {Object} PSV.plugins.VideoPlugin.Keypoint - * @property {PSV.ExtendedPosition} position - * @property {number} time - */ - -/** - * @typedef {Object} PSV.plugins.VideoPlugin.Options - * @property {boolean} [progressbar=true] - displays a progressbar on top of the navbar - * @property {boolean} [bigbutton=true] - displays a big "play" button in the center of the viewer - * @property {PSV.plugins.VideoPlugin.Keypoint[]} [keypoints] - defines autorotate timed keypoints - */ - - -// add video buttons -DEFAULTS.lang[PlayPauseButton.id] = 'Play/Pause'; -DEFAULTS.lang[VolumeButton.id] = 'Volume'; -registerButton(PlayPauseButton); -registerButton(VolumeButton); -registerButton(TimeCaption); -DEFAULTS.navbar.unshift(PlayPauseButton.groupId); - - -export { EVENTS } from './constants'; - - -/** - * @summary Controls a video adapter - * @extends PSV.plugins.AbstractPlugin - * @memberof PSV.plugins - */ -export class VideoPlugin extends AbstractPlugin { - - static id = 'video'; - - static EVENTS = EVENTS; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.plugins.VideoPlugin.Options} options - */ - constructor(psv, options) { - super(psv); - - if (!this.psv.adapter.constructor.id.includes('video')) { - throw new PSVError('VideoPlugin can only be used with a video adapter.'); - } - - /** - * @member {Object} - * @property {THREE.SplineCurve} curve - * @property {PSV.plugins.VideoPlugin.Keypoint} start - * @property {PSV.plugins.VideoPlugin.Keypoint} end - * @property {PSV.plugins.VideoPlugin.Keypoint[]} keypoints - * @private - */ - this.autorotate = { - curve : null, - start : null, - end : null, - keypoints: null, - }; - - /** - * @member {PSV.plugins.VideoPlugin.Options} - * @private - */ - this.config = { - progressbar: true, - bigbutton : true, - ...options, - }; - - if (this.config.progressbar) { - this.progressbar = new ProgressBar(this); - } - - if (this.config.bigbutton) { - this.overlay = new PauseOverlay(this); - } - - /** - * @type {PSV.plugins.MarkersPlugin} - * @private - */ - this.markers = null; - } - - /** - * @package - */ - init() { - super.init(); - - this.markers = this.psv.getPlugin('markers'); - - if (this.config.keypoints) { - this.setKeypoints(this.config.keypoints); - delete this.config.keypoints; - } - - this.psv.on(CONSTANTS.EVENTS.AUTOROTATE, this); - this.psv.on(CONSTANTS.EVENTS.BEFORE_RENDER, this); - this.psv.on(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.psv.on(CONSTANTS.EVENTS.KEY_PRESS, this); - } - - /** - * @package - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.AUTOROTATE, this); - this.psv.off(CONSTANTS.EVENTS.BEFORE_RENDER, this); - this.psv.off(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.psv.off(CONSTANTS.EVENTS.KEY_PRESS, this); - - delete this.autorotate; - delete this.progressbar; - delete this.overlay; - - super.destroy(); - } - - /** - * @private - */ - handleEvent(e) { - /* eslint-disable */ - // @formatter:off - switch (e.type) { - case CONSTANTS.EVENTS.BEFORE_RENDER: - this.__autorotate(); - break; - case CONSTANTS.EVENTS.AUTOROTATE: - this.__configureAutorotate(); - break; - case CONSTANTS.EVENTS.PANORAMA_LOADED: - this.__bindVideo(e.args[0]); - this.progressbar?.show(); - break; - case CONSTANTS.EVENTS.KEY_PRESS: - this.__onKeyPress(e, e.args[0]); - break; - case 'play': this.trigger(EVENTS.PLAY); break; - case 'pause': this.trigger(EVENTS.PAUSE); break; - case 'progress': this.trigger(EVENTS.BUFFER, this.getBufferProgress()); break; - case 'volumechange': this.trigger(EVENTS.VOLUME_CHANGE, this.getVolume()); break; - case 'timeupdate': - this.trigger(EVENTS.PROGRESS, { - time : this.getTime(), - duration: this.getDuration(), - progress: this.getProgress(), - }); - break; - } - // @formatter:on - /* eslint-enable */ - } - - /** - * @private - */ - __bindVideo(textureData) { - this.video = textureData.texture.image; - - this.video.addEventListener('play', this); - this.video.addEventListener('pause', this); - this.video.addEventListener('progress', this); - this.video.addEventListener('volumechange', this); - this.video.addEventListener('timeupdate', this); - } - - /** - * @private - */ - __onKeyPress(e, key) { - if (key === CONSTANTS.KEY_CODES.Space) { - this.playPause(); - e.preventDefault(); - } - } - - /** - * @summary Returns the durection of the video - * @returns {number} - */ - getDuration() { - return this.video?.duration ?? 0; - } - - /** - * @summary Returns the current time of the video - * @returns {number} - */ - getTime() { - return this.video?.currentTime ?? 0; - } - - /** - * @summary Returns the play progression of the video - * @returns {number} 0-1 - */ - getProgress() { - return this.video ? this.video.currentTime / this.video.duration : 0; - } - - /** - * @summary Returns if the video is playing - * @returns {boolean} - */ - isPlaying() { - return this.video ? !this.video.paused : false; - } - - /** - * @summary Returns the video volume - * @returns {number} - */ - getVolume() { - return this.video?.muted ? 0 : this.video?.volume ?? 0; - } - - /** - * @summary Starts or pause the video - */ - playPause() { - if (this.video) { - if (this.video.paused) { - this.video.play(); - } - else { - this.video.pause(); - } - } - } - - /** - * @summary Starts the video if paused - */ - play() { - if (this.video && this.video.paused) { - this.video.play(); - } - } - - /** - * @summary Pauses the cideo if playing - */ - pause() { - if (this.video && !this.video.paused) { - this.video.pause(); - } - } - - /** - * @summary Sets the volume of the video - * @param {number} volume - */ - setVolume(volume) { - if (this.video) { - this.video.muted = false; - this.video.volume = volume; - } - } - - /** - * @summary (Un)mutes the video - * @param {boolean} [mute] - toggle if undefined - */ - setMute(mute) { - if (this.video) { - this.video.muted = mute === undefined ? !this.video.muted : mute; - if (!this.video.muted && this.video.volume === 0) { - this.video.volume = 0.1; - } - } - } - - /** - * @summary Changes the current time of the video - * @param {number} time - */ - setTime(time) { - if (this.video) { - this.video.currentTime = time; - } - } - - /** - * @summary Changes the progression of the video - * @param {number} progress 0-1 - */ - setProgress(progress) { - if (this.video) { - this.video.currentTime = this.video.duration * progress; - } - } - - getBufferProgress() { - if (this.video) { - let maxBuffer = 0; - - const buffer = this.video.buffered; - - for (let i = 0, l = buffer.length; i < l; i++) { - if (buffer.start(i) <= this.video.currentTime && buffer.end(i) >= this.video.currentTime) { - maxBuffer = buffer.end(i); - break; - } - } - - return Math.max(this.video.currentTime, maxBuffer) / this.video.duration; - } - else { - return 0; - } - } - - /** - * @summary Changes the keypoints - * @param {PSV.plugins.VideoPlugin.Keypoint[]} keypoints - */ - setKeypoints(keypoints) { - if (keypoints && keypoints.length < 2) { - throw new PSVError('At least two points are required'); - } - - this.autorotate.keypoints = utils.clone(keypoints); - - if (this.autorotate.keypoints) { - this.autorotate.keypoints.forEach((pt, i) => { - if (pt.position) { - const position = this.psv.dataHelper.cleanPosition(pt.position); - pt.position = [position.longitude, position.latitude]; - } - else { - throw new PSVError(`Keypoint #${i} is missing marker or position`); - } - - if (utils.isNil(pt.time)) { - throw new PSVError(`Keypoint #${i} is missing time`); - } - }); - - this.autorotate.keypoints.sort((a, b) => a.time - b.time); - } - - this.__configureAutorotate(); - } - - /** - * @private - */ - __configureAutorotate() { - delete this.autorotate.curve; - delete this.autorotate.start; - delete this.autorotate.end; - - if (this.psv.isAutorotateEnabled() && this.autorotate.keypoints) { - // cancel core rotation - this.psv.dynamics.position.stop(); - } - } - - /** - * @private - */ - __autorotate() { - if (!this.psv.isAutorotateEnabled() || !this.autorotate.keypoints) { - return; - } - - const currentTime = this.getTime(); - const autorotate = this.autorotate; - - if (!autorotate.curve || currentTime < autorotate.start.time || currentTime >= autorotate.end.time) { - this.__autorotateNext(currentTime); - } - - if (autorotate.start === autorotate.end) { - this.psv.rotate({ - longitude: autorotate.start.position[0], - latitude : autorotate.start.position[1], - }); - } - else { - const progress = (currentTime - autorotate.start.time) / (autorotate.end.time - autorotate.start.time); - // only the middle segment contains the current section - const pt = autorotate.curve.getPoint(1 / 3 + progress / 3); - - this.psv.dynamics.position.goto({ - longitude: pt.x, - latitude : pt.y, - }); - } - } - - /** - * @private - */ - __autorotateNext(currentTime) { - let k1 = null; - let k2 = null; - - const keypoints = this.autorotate.keypoints; - const l = keypoints.length - 1; - - if (currentTime < keypoints[0].time) { - k1 = 0; - k2 = 0; - } - for (let i = 0; i < l; i++) { - if (currentTime >= keypoints[i].time && currentTime < keypoints[i + 1].time) { - k1 = i; - k2 = i + 1; - break; - } - } - if (currentTime >= keypoints[l].time) { - k1 = l; - k2 = l; - } - - // get the 4 points necessary to compute the current movement - // one point before and two points after the current - const workPoints = [ - keypoints[Math.max(0, k1 - 1)].position, - keypoints[k1].position, - keypoints[k2].position, - keypoints[Math.min(l, k2 + 1)].position, - ]; - - // apply offsets to avoid crossing the origin - const workVectors = [new Vector2(workPoints[0][0], workPoints[0][1])]; - - let k = 0; - for (let i = 1; i <= 3; i++) { - const d = workPoints[i - 1][0] - workPoints[i][0]; - if (d > Math.PI) { // crossed the origin left to right - k += 1; - } - else if (d < -Math.PI) { // crossed the origin right to left - k -= 1; - } - if (k !== 0 && i === 1) { - // do not modify first point, apply the reverse offset the the previous point instead - workVectors[0].x -= k * 2 * Math.PI; - k = 0; - } - workVectors.push(new Vector2(workPoints[i][0] + k * 2 * Math.PI, workPoints[i][1])); - } - - this.autorotate.curve = new SplineCurve(workVectors); - this.autorotate.start = keypoints[k1]; - this.autorotate.end = keypoints[k2]; - - // debugCurve(this.markers, this.autorotate.curve.getPoints(16 * 3).map(p => ([p.x, p.y])), 16); - } - -} diff --git a/src/plugins/video/style.scss b/src/plugins/video/style.scss deleted file mode 100644 index 06342917e..000000000 --- a/src/plugins/video/style.scss +++ /dev/null @@ -1,200 +0,0 @@ -@use 'sass:map'; -@import '../../styles/vars'; - -$psv-progressbar-height: 3px !default; -$psv-progressbar-height-active: 5px !default; -$psv-progressbar-container: 20px !default; -$psv-progressbar-progress-color: $psv-buttons-color !default; -$psv-progressbar-buffer-color: $psv-buttons-active-background !default; -$psv-progressbar-handle-size: 9px !default; -$psv-progressbar-handle-color: white !default; - -$psv-volume-height: 80px !default; -$psv-volume-width: $psv-progressbar-height-active !default; -$psv-volume-bar-color: $psv-progressbar-progress-color !default; -$psv-volume-track-color: $psv-progressbar-buffer-color !default; -$psv-volume-handle-size: $psv-progressbar-handle-size !default; -$psv-volume-handle-color: $psv-progressbar-handle-color !default; - -$psv-video-bigbutton-size: (portrait: 20vw, landscape: 10vw) !default; -$psv-video-bigbutton-color: $psv-buttons-color !default; - -.psv-video { - &-progressbar { - position: absolute; - left: 0; - bottom: 0; - width: 100%; - height: $psv-progressbar-container; - cursor: pointer; - z-index: $psv-navbar-zindex - 1; - - @at-root .psv--has-navbar & { - bottom: $psv-navbar-height; - } - - & > * { - position: absolute; - left: 0; - bottom: 0; - width: 100%; - height: $psv-progressbar-height; - transition: height .2s linear; - } - - &:hover > * { - height: $psv-progressbar-height-active; - } - - &__progress { - background-color: $psv-progressbar-progress-color; - } - - &__buffer { - background-color: $psv-progressbar-buffer-color; - } - - &__handle { - display: none; - height: $psv-progressbar-handle-size !important; - width: $psv-progressbar-handle-size; - border-radius: 50%; - margin-bottom: #{- ($psv-progressbar-handle-size - $psv-progressbar-height-active) * .5}; - margin-left: #{- $psv-progressbar-handle-size * .5}; - background: $psv-progressbar-handle-color; - } - } - - &-time { - flex: 0 0 auto; - - .psv-caption-content { - min-width: 6em; - text-align: center; - } - } - - &-volume { - &__container { - position: absolute; - left: 0; - bottom: $psv-navbar-height; - padding: $psv-buttons-height 0; - width: $psv-navbar-height; - height: 0; - opacity: 0; - background: $psv-navbar-background; - transition: opacity .2s linear, height .3s step-end; - } - - &__range { - position: relative; - height: $psv-volume-height; - } - - &__progress, - &__track { - position: absolute; - bottom: 0; - left: #{($psv-navbar-height - $psv-volume-width) * .5}; - width: $psv-volume-width; - background: $psv-volume-bar-color; - } - - &__track { - height: 100%; - background: $psv-volume-track-color; - } - - &__handle { - position: absolute; - left: #{($psv-navbar-height - $psv-volume-width) * .5}; - height: $psv-volume-handle-size; - width: $psv-volume-handle-size; - border-radius: 50%; - margin-left: #{- ($psv-volume-handle-size - $psv-volume-width) * .5}; - margin-bottom: #{- $psv-volume-handle-size * .5}; - background: $psv-volume-handle-color; - } - } - - &-volume-button { - position: relative; - - &:hover .psv-video-volume__container { - height: $psv-volume-height; - opacity: 1; - transition-timing-function: linear, step-start; - transition-delay: .3s; - } - - &--0 .psv-button-svg { - #lvl1, - #lvl2, - #lvl3 { - fill: none; - } - } - - &--1 .psv-button-svg { - #lvl0, - #lvl2, - #lvl3 { - fill: none; - } - } - - &--2 .psv-button-svg { - #lvl0, - #lvl3 { - fill: none; - } - } - - &--3 .psv-button-svg { - #lvl0 { - fill: none; - } - } - } - - &-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - z-index: $psv-overlay-zindex; - pointer-events: none; - } - - &-bigbutton { - display: block; - border: none; - background: none; - padding: 0; - color: $psv-video-bigbutton-color; - pointer-events: auto; - cursor: pointer; - opacity: 0; - width: 0; - transition: opacity .2s linear, width .3s step-end; - - &--pause { - width: map.get($psv-video-bigbutton-size, portrait); - opacity: 1; - transition-timing-function: linear, step-start; - - @media (orientation: landscape) { - width: map.get($psv-video-bigbutton-size, landscape); - } - } - - svg { - width: 100%; - } - } -} diff --git a/src/plugins/video/utils.js b/src/plugins/video/utils.js deleted file mode 100644 index 5428e93de..000000000 --- a/src/plugins/video/utils.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @private - */ -export function formatTime(time) { - const seconds = Math.round(time % 60); - const minutes = Math.round(time - seconds) / 60; - return `${minutes}:${('0' + seconds).slice(-2)}`; -} diff --git a/src/plugins/virtual-tour/AbstractDatasource.js b/src/plugins/virtual-tour/AbstractDatasource.js deleted file mode 100644 index fb024079a..000000000 --- a/src/plugins/virtual-tour/AbstractDatasource.js +++ /dev/null @@ -1,34 +0,0 @@ -import { PSVError } from 'photo-sphere-viewer'; - -/** - * @memberOf PSV.plugins.VirtualTourPlugin - * @private - */ -export class AbstractDatasource { - - /** - * @type {Record} - */ - nodes = {}; - - /** - * @param {PSV.plugins.VirtualTourPlugin} plugin - */ - constructor(plugin) { - this.plugin = plugin; - } - - destroy() { - delete this.plugin; - } - - /** - * @summary Loads a node - * @param {string} nodeId - * @return {Promise} - */ - loadNode(nodeId) { // eslint-disable-line no-unused-vars - throw new PSVError('loadNode not implemented'); - } - -} diff --git a/src/plugins/virtual-tour/ClientSideDatasource.js b/src/plugins/virtual-tour/ClientSideDatasource.js deleted file mode 100644 index 006e97a33..000000000 --- a/src/plugins/virtual-tour/ClientSideDatasource.js +++ /dev/null @@ -1,67 +0,0 @@ -import { PSVError, utils } from 'photo-sphere-viewer'; -import { AbstractDatasource } from './AbstractDatasource'; -import { checkLink, checkNode } from './utils'; - -/** - * @memberOf PSV.plugins.VirtualTourPlugin - * @private - */ -export class ClientSideDatasource extends AbstractDatasource { - - loadNode(nodeId) { - if (this.nodes[nodeId]) { - return Promise.resolve(this.nodes[nodeId]); - } - else { - return Promise.reject(new PSVError(`Node ${nodeId} not found`)); - } - } - - setNodes(rawNodes) { - if (!rawNodes?.length) { - throw new PSVError('No nodes provided'); - } - - const nodes = {}; - const linkedNodes = {}; - - rawNodes.forEach((node) => { - checkNode(node, this.plugin.isGps()); - - if (nodes[node.id]) { - throw new PSVError(`Duplicate node ${node.id}`); - } - if (!node.links) { - utils.logWarn(`Node ${node.id} has no links`); - node.links = []; - } - - nodes[node.id] = node; - }); - - rawNodes.forEach((node) => { - node.links.forEach((link) => { - if (!nodes[link.nodeId]) { - throw new PSVError(`Target node ${link.nodeId} of node ${node.id} does not exists`); - } - - // copy essential data - link.position = link.position || nodes[link.nodeId].position; - link.name = link.name || nodes[link.nodeId].name; - - checkLink(node, link, this.plugin.isGps()); - - linkedNodes[link.nodeId] = true; - }); - }); - - rawNodes.forEach((node) => { - if (!linkedNodes[node.id]) { - utils.logWarn(`Node ${node.id} is never linked to`); - } - }); - - this.nodes = nodes; - } - -} diff --git a/src/plugins/virtual-tour/ServerSideDatasource.js b/src/plugins/virtual-tour/ServerSideDatasource.js deleted file mode 100644 index 518fce5ca..000000000 --- a/src/plugins/virtual-tour/ServerSideDatasource.js +++ /dev/null @@ -1,50 +0,0 @@ -import { PSVError, utils } from 'photo-sphere-viewer'; -import { AbstractDatasource } from './AbstractDatasource'; -import { checkLink, checkNode } from './utils'; - -/** - * @memberOf PSV.plugins.VirtualTourPlugin - * @private - */ -export class ServerSideDatasource extends AbstractDatasource { - - constructor(plugin) { - super(plugin); - - if (!plugin.config.getNode) { - throw new PSVError('Missing getNode() option.'); - } - - this.nodeResolver = plugin.config.getNode; - } - - loadNode(nodeId) { - if (this.nodes[nodeId]) { - return Promise.resolve(this.nodes[nodeId]); - } - else { - return Promise.resolve(this.nodeResolver(nodeId)) - .then((node) => { - checkNode(node, this.plugin.isGps()); - if (!node.links) { - utils.logWarn(`Node ${node.id} has no links`); - node.links = []; - } - - node.links.forEach((link) => { - // copy essential data - if (this.nodes[link.nodeId]) { - link.position = link.position || this.nodes[link.nodeId].position; - link.name = link.name || this.nodes[link.nodeId].name; - } - - checkLink(node, link, this.plugin.isGps()); - }); - - this.nodes[nodeId] = node; - return node; - }); - } - } - -} diff --git a/src/plugins/virtual-tour/constants.js b/src/plugins/virtual-tour/constants.js deleted file mode 100644 index 2ca737050..000000000 --- a/src/plugins/virtual-tour/constants.js +++ /dev/null @@ -1,126 +0,0 @@ -import { ObjectLoader } from 'three'; -import arrowGeometryJson from './arrow.json'; -import arrowIconSvg from './arrow.svg'; -import arrowOutlineGeometryJson from './arrow_outline.json'; - -/** - * @summary In client mode all the nodes are provided in the config or with the `setNodes` method - * @type {string} - * @memberof PSV.plugins.VirtualTourPlugin - * @constant - */ -export const MODE_CLIENT = 'client'; - -/** - * @summary In server mode the nodes are fetched asynchronously - * @type {string} - * @memberof PSV.plugins.VirtualTourPlugin - * @constant - */ -export const MODE_SERVER = 'server'; - -/** - * @summary In manual mode each link is positionned manually on the panorama - * @type {string} - * @memberof PSV.plugins.VirtualTourPlugin - * @constant - */ -export const MODE_MANUAL = 'manual'; - -/** - * @summary In GPS mode each node is globally positionned and the links are automatically computed - * @type {string} - * @memberof PSV.plugins.VirtualTourPlugin - * @constant - */ -export const MODE_GPS = 'gps'; - -/** - * @summaru In markers mode the links are represented using markers - * @type {string} - * @memberof PSV.plugins.VirtualTourPlugin - * @constant - */ -export const MODE_MARKERS = 'markers'; - -/** - * @summaru In 3D mode the links are represented using 3d arrows - * @type {string} - * @memberof PSV.plugins.VirtualTourPlugin - * @constant - */ -export const MODE_3D = '3d'; - -/** - * @summary Available events - * @enum {string} - * @memberof PSV.plugins.VirtualTourPlugin - * @constant - */ -export const EVENTS = { - /** - * @event node-changed - * @memberof PSV.plugins.VirtualTourPlugin - * @summary Triggered when the current node changes - * @param {string} nodeId - */ - NODE_CHANGED : 'node-changed', -}; - -/** - * @summary Property name added to markers - * @type {string} - * @constant - * @private - */ -export const LINK_DATA = 'tourLink'; - -/** - * @summary Default style of the link marker - * @type {PSV.plugins.MarkersPlugin.Properties} - * @constant - * @private - */ -export const DEFAULT_MARKER = { - html : arrowIconSvg, - width : 80, - height : 80, - scale : [0.5, 2], - anchor : 'top center', - className: 'psv-virtual-tour__marker', - style : { - color: 'rgba(0, 208, 255, 0.8)', - }, -}; - -/** - * @summary Default style of the link arrow - * @type {PSV.plugins.VirtualTourPlugin.ArrowStyle} - * @constant - * @private - */ -export const DEFAULT_ARROW = { - color : 0xaaaaaa, - hoverColor : 0xaa5500, - outlineColor: 0x000000, - scale : [0.5, 2], -}; - -/** - * @type {external:THREE.BufferedGeometry} - * @constant - * @private - */ -export const { ARROW_GEOM, ARROW_OUTLINE_GEOM } = (() => { - const loader = new ObjectLoader(); - const geometries = loader.parseGeometries([arrowGeometryJson, arrowOutlineGeometryJson]); - const arrow = geometries[arrowGeometryJson.uuid]; - const arrowOutline = geometries[arrowOutlineGeometryJson.uuid]; - const scale = 0.015; - const rot = Math.PI / 2; - arrow.scale(scale, scale, scale); - arrow.rotateX(rot); - arrowOutline.scale(scale, scale, scale); - arrowOutline.rotateX(rot); - return { ARROW_GEOM: arrow, ARROW_OUTLINE_GEOM: arrowOutline }; -})(); diff --git a/src/plugins/virtual-tour/index.js b/src/plugins/virtual-tour/index.js deleted file mode 100644 index 48ad9013d..000000000 --- a/src/plugins/virtual-tour/index.js +++ /dev/null @@ -1,720 +0,0 @@ -import { - AmbientLight, - BackSide, - Group, - MathUtils, - Mesh, - MeshBasicMaterial, - MeshLambertMaterial, - PointLight -} from 'three'; -import { AbstractPlugin, CONSTANTS, PSVError, utils } from '../..'; -import { ClientSideDatasource } from './ClientSideDatasource'; -import { - ARROW_GEOM, - ARROW_OUTLINE_GEOM, - DEFAULT_ARROW, - DEFAULT_MARKER, - EVENTS, - LINK_DATA, - MODE_3D, - MODE_CLIENT, - MODE_GPS, - MODE_MANUAL, - MODE_MARKERS, - MODE_SERVER -} from './constants'; -import { ServerSideDatasource } from './ServerSideDatasource'; -import './style.scss'; -import { bearing, distance, setMeshColor } from './utils'; - - -/** - * @callback GetNode - * @summary Function to load a node - * @memberOf PSV.plugins.VirtualTourPlugin - * @param {string} nodeId - * @returns {PSV.plugins.VirtualTourPlugin.Node|Promise} - */ - -/** - * @callback Preload - * @summary Function to determine if a link must be preloaded - * @memberOf PSV.plugins.VirtualTourPlugin - * @param {PSV.plugins.VirtualTourPlugin.Node} node - * @param {PSV.plugins.VirtualTourPlugin.NodeLink} link - * @returns {boolean} - */ - -/** - * @typedef {Object} PSV.plugins.VirtualTourPlugin.Node - * @summary Definition of a single node in the tour - * @property {string} id - unique identifier of the node - * @property {*} panorama - * @property {PSV.plugins.VirtualTourPlugin.NodeLink[]} [links] - links to other nodes - * @property {number[]} [position] - GPS position (longitude, latitude, optional altitude) - * @property {PSV.PanoData | PSV.PanoDataProvider} [panoData] - data used for this panorama - * @property {PSV.SphereCorrection} [sphereCorrection] - sphere correction to apply to this panorama - * @property {string} [name] - short name of the node - * @property {string} [caption] - caption visible in the navbar - * @property {string} [description] - description visible in the side panel - * @property {string} [thumbnail] - thumbnail for the gallery - * @property {PSV.plugins.MarkersPlugin.Properties[]} [markers] - additional markers to use on this node - */ - -/** - * @typedef {PSV.ExtendedPosition} PSV.plugins.VirtualTourPlugin.NodeLink - * @summary Definition of a link between two nodes - * @property {string} nodeId - identifier of the target node - * @property {string} [name] - override the name of the node (tooltip) - * @property {number[]} [position] - override the GPS position of the node - * @property {PSV.plugins.MarkersPlugin.Properties} [markerStyle] - override global marker style - * @property {PSV.plugins.VirtualTourPlugin.ArrowStyle} [arrowStyle] - override global arrow style - */ - -/** - * @typedef {Object} PSV.plugins.VirtualTourPlugin.ArrowStyle - * @summary Style of the arrow in 3D mode - * @property {string} [color=0xaaaaaa] - * @property {string} [hoverColor=0xaa5500] - * @property {number} [outlineColor=0x000000] - * @property {number[]} [scale=[0.5,2]] - */ - -/** - * @typedef {Object} PSV.plugins.VirtualTourPlugin.Options - * @property {'client'|'server'} [dataMode='client'] - configure data mode - * @property {'manual'|'gps'} [positionMode='manual'] - configure positioning mode - * @property {'markers'|'3d'} [renderMode='3d'] - configure rendering mode of links - * @property {PSV.plugins.VirtualTourPlugin.Node[]} [nodes] - initial nodes - * @property {PSV.plugins.VirtualTourPlugin.GetNode} [getNode] - * @property {string} [startNodeId] - id of the initial node, if not defined the first node will be used - * @property {boolean|PSV.plugins.VirtualTourPlugin.Preload} [preload=false] - preload linked panoramas - * @property {boolean|string|number} [rotateSpeed='20rpm'] - speed of rotation when clicking on a link, if 'false' the viewer won't rotate at all - * @property {boolean|number} [transition=1500] - duration of the transition between nodes - * @property {boolean} [linksOnCompass] - if the Compass plugin is enabled, displays the links on the compass, defaults to `true` on in markers render mode - * @property {PSV.plugins.MarkersPlugin.Properties} [markerStyle] - global marker style - * @property {PSV.plugins.VirtualTourPlugin.ArrowStyle} [arrowStyle] - global arrow style - * @property {number} [markerLatOffset=-0.1] - (GPS & Markers mode) latitude offset applied to link markers, to compensate for viewer height - * @property {'top'|'bottom'} [arrowPosition='bottom'] - (3D mode) arrows vertical position - */ - -/** - * @typedef {Object} PSV.plugins.VirtualTourPlugin.NodeChangedData - * @summary Data associated to the "node-changed" event - * @type {PSV.plugins.VirtualTourPlugin.Node} [fromNode] - The previous node - * @type {PSV.plugins.VirtualTourPlugin.NodeLink} [fromLink] - The link that was clicked in the previous node - * @type {PSV.Position} [fromLinkPosition] - The position of the link on the previous node - */ - - -export { EVENTS, MODE_3D, MODE_CLIENT, MODE_GPS, MODE_MANUAL, MODE_MARKERS, MODE_SERVER } from './constants'; - - -/** - * @summary Create virtual tours by linking multiple panoramas - * @extends PSV.plugins.AbstractPlugin - * @memberof PSV.plugins - */ -export class VirtualTourPlugin extends AbstractPlugin { - - static id = 'virtual-tour'; - - static EVENTS = EVENTS; - static MODE_CLIENT = MODE_CLIENT; - static MODE_SERVER = MODE_SERVER; - static MODE_3D = MODE_3D; - static MODE_MARKERS = MODE_MARKERS; - static MODE_MANUAL = MODE_MANUAL; - static MODE_GPS = MODE_GPS; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.plugins.VirtualTourPlugin.Options} [options] - */ - constructor(psv, options) { - super(psv); - - /** - * @member {Object} - * @property {PSV.plugins.VirtualTourPlugin.Node} currentNode - * @property {PSV.Tooltip} currentTooltip - * @property {string} loadingNode - * @property {function} stopObserver - * @private - */ - this.prop = { - currentNode : null, - currentTooltip: null, - loadingNode : null, - stopObserver : null, - }; - - /** - * @type {Record} - * @private - */ - this.preload = {}; - - /** - * @member {PSV.plugins.VirtualTourPlugin.Options} - * @private - */ - this.config = { - dataMode : MODE_CLIENT, - positionMode : MODE_MANUAL, - renderMode : MODE_3D, - preload : false, - rotateSpeed : '20rpm', - transition : CONSTANTS.DEFAULT_TRANSITION, - markerLatOffset: -0.1, - arrowPosition : 'bottom', - linksOnCompass : options?.renderMode === MODE_MARKERS, - ...options, - markerStyle: { - ...DEFAULT_MARKER, - ...options?.markerStyle, - }, - arrowStyle : { - ...DEFAULT_ARROW, - ...options?.arrowStyle, - }, - }; - - /** - * @type {PSV.plugins.MarkersPlugin} - * @private - */ - this.markers = null; - - /** - * @type {PSV.plugins.CompassPlugin} - * @private - */ - this.compass = null; - - /** - * @type {PSV.plugins.GalleryPlugin} - * @private - */ - this.gallery = null; - - /** - * @type {PSV.plugins.VirtualTourPlugin.AbstractDatasource} - */ - this.datasource = null; - - /** - * @type {external:THREE.Group} - * @private - */ - this.arrowsGroup = null; - - if (this.is3D()) { - this.arrowsGroup = new Group(); - - const localLight = new PointLight(0xffffff, 1, 0); - localLight.position.set(0, this.config.arrowPosition === 'bottom' ? 2 : -2, 0); - this.arrowsGroup.add(localLight); - } - } - - /** - * @package - */ - init() { - super.init(); - - this.markers = this.psv.getPlugin('markers'); - this.compass = this.psv.getPlugin('compass'); - this.gallery = this.psv.getPlugin('gallery'); - - if (!this.is3D() && !this.markers) { - throw new PSVError('VirtualTour plugin requires the Markers plugin in markers mode.'); - } - - if (this.markers?.config.markers) { - utils.logWarn('No default markers can be configured on Markers plugin when using VirtualTour plugin. ' - + 'Consider defining `markers` on each tour node.'); - delete this.markers.config.markers; - } - - this.datasource = this.isServerSide() ? new ServerSideDatasource(this) : new ClientSideDatasource(this); - - if (this.is3D()) { - this.psv.once(CONSTANTS.EVENTS.READY, () => { - this.__positionArrows(); - this.psv.renderer.scene.add(this.arrowsGroup); - - const ambientLight = new AmbientLight(0xffffff, 1); - this.psv.renderer.scene.add(ambientLight); - - this.psv.needsUpdate(); - }); - - this.psv.on(CONSTANTS.EVENTS.POSITION_UPDATED, this); - this.psv.on(CONSTANTS.EVENTS.ZOOM_UPDATED, this); - this.psv.on(CONSTANTS.EVENTS.CLICK, this); - this.prop.stopObserver = this.psv.observeObjects(LINK_DATA, this); - } - else { - this.markers.on('select-marker', this); - } - - if (this.isServerSide()) { - if (this.config.startNodeId) { - this.setCurrentNode(this.config.startNodeId); - } - } - else if (this.config.nodes) { - this.setNodes(this.config.nodes, this.config.startNodeId); - delete this.config.nodes; - } - } - - /** - * @package - */ - destroy() { - if (this.markers) { - this.markers.off('select-marker', this); - } - if (this.arrowsGroup) { - this.psv.renderer.scene.remove(this.arrowsGroup); - } - - this.psv.off(CONSTANTS.EVENTS.POSITION_UPDATED, this); - this.psv.off(CONSTANTS.EVENTS.ZOOM_UPDATED, this); - this.psv.off(CONSTANTS.EVENTS.CLICK, this); - this.prop.stopObserver?.(); - - this.datasource.destroy(); - - delete this.preload; - delete this.datasource; - delete this.markers; - delete this.compass; - delete this.gallery; - delete this.arrowsGroup; - - super.destroy(); - } - - handleEvent(e) { - let link; - switch (e.type) { - case 'select-marker': - link = e.args[0].data?.[LINK_DATA]; - if (link) { - this.setCurrentNode(link.nodeId, link); - } - break; - - case CONSTANTS.EVENTS.POSITION_UPDATED: - case CONSTANTS.EVENTS.ZOOM_UPDATED: - if (this.arrowsGroup) { - this.__positionArrows(); - } - break; - - case CONSTANTS.EVENTS.CLICK: - link = e.args[0].objects.find(o => o.userData[LINK_DATA])?.userData[LINK_DATA]; - if (link) { - this.setCurrentNode(link.nodeId, link); - } - break; - - case CONSTANTS.OBJECT_EVENTS.ENTER_OBJECT: - this.__onEnterObject(e.detail.object, e.detail.viewerPoint); - break; - case CONSTANTS.OBJECT_EVENTS.HOVER_OBJECT: - this.__onHoverObject(e.detail.object, e.detail.viewerPoint); - break; - case CONSTANTS.OBJECT_EVENTS.LEAVE_OBJECT: - this.__onLeaveObject(e.detail.object); - break; - - default: - } - } - - /** - * @summary Tests if running in server mode - * @return {boolean} - */ - isServerSide() { - return this.config.dataMode === MODE_SERVER; - } - - /** - * @summary Tests if running in GPS mode - * @return {boolean} - */ - isGps() { - return this.config.positionMode === MODE_GPS; - } - - /** - * @summary Tests if running in 3D mode - * @return {boolean} - */ - is3D() { - return this.config.renderMode === MODE_3D; - } - - /** - * @summary Sets the nodes (client mode only) - * @param {PSV.plugins.VirtualTourPlugin.Node[]} nodes - * @param {string} [startNodeId] - * @throws {PSV.PSVError} when the configuration is incorrect - */ - setNodes(nodes, startNodeId) { - if (this.isServerSide()) { - throw new PSVError('Cannot set nodes in server side mode'); - } - - this.datasource.setNodes(nodes); - - if (!startNodeId) { - startNodeId = nodes[0].id; - } - else if (!this.datasource.nodes[startNodeId]) { - startNodeId = nodes[0].id; - utils.logWarn(`startNodeId not found is provided nodes, resetted to ${startNodeId}`); - } - - this.setCurrentNode(startNodeId); - - if (this.gallery) { - this.gallery.setItems( - nodes.map(node => ({ - id : node.id, - panorama : node.panorama, - name : node.name, - thumbnail: node.thumbnail, - options : { - caption : node.caption, - panoData : node.panoData, - sphereCorrection: node.sphereCorrection, - description : node.description, - }, - })), - (id) => { - this.setCurrentNode(id); - this.gallery.hide(); - } - ); - } - } - - /** - * @summary Changes the current node - * @param {string} nodeId - * @param {PSV.plugins.VirtualTourPlugin.NodeLink} [fromLink] - * @returns {Promise} resolves false if the loading was aborted by another call - */ - setCurrentNode(nodeId, fromLink = null) { - if (nodeId === this.prop.currentNode?.id) { - return Promise.resolve(true); - } - - this.psv.hideError(); - - this.prop.loadingNode = nodeId; - - const fromNode = this.prop.currentNode; - const fromLinkPosition = fromNode && fromLink ? this.__getLinkPosition(fromNode, fromLink) : null; - - return Promise.all([ - // if this node is already preloading, wait for it - Promise.resolve(this.preload[nodeId]) - .then(() => { - if (this.prop.loadingNode !== nodeId) { - throw utils.getAbortError(); - } - - return this.datasource.loadNode(nodeId); - }), - Promise.resolve(fromLinkPosition ? this.config.rotateSpeed : false) - .then((speed) => { // eslint-disable-line consistent-return - if (speed) { - return this.psv.animate({ ...fromLinkPosition, speed }); - } - }) - .then(() => { - this.psv.loader.show(); - }), - ]) - .then(([node]) => { - if (this.prop.loadingNode !== nodeId) { - throw utils.getAbortError(); - } - - this.prop.currentNode = node; - - if (this.prop.currentTooltip) { - this.prop.currentTooltip.hide(); - this.prop.currentTooltip = null; - } - - if (this.is3D()) { - this.arrowsGroup.remove(...this.arrowsGroup.children.filter(o => o.type === 'Mesh')); - } - - this.markers?.clearMarkers(); - this.compass?.clearHotspots(); - - return this.psv.setPanorama(node.panorama, { - transition : this.config.transition, - caption : node.caption, - description : node.description, - panoData : node.panoData, - sphereCorrection: node.sphereCorrection, - }) - .then((completed) => { - if (!completed) { - throw utils.getAbortError(); - } - }); - }) - .then(() => { - if (this.prop.loadingNode !== nodeId) { - throw utils.getAbortError(); - } - - const node = this.prop.currentNode; - - if (node.markers) { - if (this.markers) { - this.markers.setMarkers(node.markers); - } - else { - utils.logWarn(`Node ${node.id} markers ignored because the plugin is not loaded.`); - } - } - - this.__renderLinks(node); - this.__preload(node); - - /** - * @event node-changed - * @memberof PSV.plugins.VirtualTourPlugin - * @summary Triggered when the current node is changed - * @param {string} nodeId - * @param {PSV.plugins.VirtualTourPlugin.NodeChangedData} data - */ - this.trigger(EVENTS.NODE_CHANGED, nodeId, { - fromNode, - fromLink, - fromLinkPosition, - }); - - this.prop.loadingNode = null; - - return true; - }) - .catch((err) => { - if (utils.isAbortError(err)) { - return false; - } - - this.psv.showError(this.psv.config.lang.loadError); - - this.psv.loader.hide(); - this.psv.navbar.setCaption(''); - - this.prop.loadingNode = null; - - throw err; - }); - } - - /** - * @summary Adds the links for the node - * @param {PSV.plugins.VirtualTourPlugin.Node} node - * @private - */ - __renderLinks(node) { - const positions = []; - - node.links.forEach((link) => { - const position = this.__getLinkPosition(node, link); - positions.push(position); - - if (this.is3D()) { - const mesh = new Mesh(ARROW_GEOM, new MeshLambertMaterial()); - mesh.userData = { [LINK_DATA]: link, longitude: position.longitude }; - mesh.rotation.order = 'YXZ'; - mesh.rotateY(-position.longitude); - this.psv.dataHelper - .sphericalCoordsToVector3({ longitude: position.longitude, latitude: 0 }, mesh.position) - .multiplyScalar(1 / CONSTANTS.SPHERE_RADIUS); - - const outlineMesh = new Mesh(ARROW_OUTLINE_GEOM, new MeshBasicMaterial({ side: BackSide })); - outlineMesh.position.copy(mesh.position); - outlineMesh.rotation.copy(mesh.rotation); - - setMeshColor(mesh, link.arrowStyle?.color || this.config.arrowStyle.color); - setMeshColor(outlineMesh, link.arrowStyle?.outlineColor || this.config.arrowStyle.outlineColor); - - this.arrowsGroup.add(mesh); - this.arrowsGroup.add(outlineMesh); - } - else { - if (this.isGps()) { - position.latitude += this.config.markerLatOffset; - } - - this.markers.addMarker({ - ...this.config.markerStyle, - ...link.markerStyle, - id : `tour-link-${link.nodeId}`, - tooltip : link.name, - visible : true, - hideList: true, - content : null, - data : { [LINK_DATA]: link }, - ...position, - }, false); - } - }); - - if (this.is3D()) { - this.__positionArrows(); - } - else { - this.markers.renderMarkers(); - } - - if (this.config.linksOnCompass && this.compass) { - this.compass.setHotspots(positions); - } - } - - /** - * @summary Computes the marker position for a link - * @param {PSV.plugins.VirtualTourPlugin.Node} node - * @param {PSV.plugins.VirtualTourPlugin.NodeLink} link - * @return {PSV.Position} - * @private - */ - __getLinkPosition(node, link) { - if (this.isGps()) { - const p1 = [MathUtils.degToRad(node.position[0]), MathUtils.degToRad(node.position[1])]; - const p2 = [MathUtils.degToRad(link.position[0]), MathUtils.degToRad(link.position[1])]; - const h1 = node.position[2] !== undefined ? node.position[2] : link.position[2] || 0; - const h2 = link.position[2] !== undefined ? link.position[2] : node.position[2] || 0; - - let latitude = 0; - if (h1 !== h2) { - latitude = Math.atan((h2 - h1) / distance(p1, p2)); - } - - const longitude = bearing(p1, p2); - - return { longitude, latitude }; - } - else { - return this.psv.dataHelper.cleanPosition(link); - } - } - - /** - * @private - */ - __onEnterObject(mesh, viewerPoint) { - const link = mesh.userData[LINK_DATA]; - - setMeshColor(mesh, link.arrowStyle?.hoverColor || this.config.arrowStyle.hoverColor); - - if (link.name) { - this.prop.currentTooltip = this.psv.tooltip.create({ - left : viewerPoint.x, - top : viewerPoint.y, - content: link.name, - }); - } - - this.psv.needsUpdate(); - } - - - /** - * @private - */ - __onHoverObject(mesh, viewerPoint) { - if (this.prop.currentTooltip) { - this.prop.currentTooltip.move({ - left: viewerPoint.x, - top : viewerPoint.y, - }); - } - } - - - /** - * @private - */ - __onLeaveObject(mesh) { - const link = mesh.userData[LINK_DATA]; - - setMeshColor(mesh, link.arrowStyle?.color || this.config.arrowStyle.color); - - if (this.prop.currentTooltip) { - this.prop.currentTooltip.hide(); - this.prop.currentTooltip = null; - } - - this.psv.needsUpdate(); - } - - /** - * @summary Updates to position of the group of arrows - * @private - */ - __positionArrows() { - this.arrowsGroup.position.copy(this.psv.prop.direction).multiplyScalar(0.5); - const s = this.config.arrowStyle.scale; - const f = s[1] + (s[0] - s[1]) * (this.psv.getZoomLevel() / 100); - const y = 2.5 - (this.psv.getZoomLevel() / 100) * 1.5; - this.arrowsGroup.position.y += this.config.arrowPosition === 'bottom' ? -y : y; - this.arrowsGroup.scale.set(f, f, f); - } - - /** - * @summary Manage the preload of the linked panoramas - * @param {PSV.plugins.VirtualTourPlugin.Node} node - * @private - */ - __preload(node) { - if (!this.config.preload) { - return; - } - - this.preload[node.id] = true; - - this.prop.currentNode.links - .filter(link => !this.preload[link.nodeId]) - .filter((link) => { - if (typeof this.config.preload === 'function') { - return this.config.preload(this.prop.currentNode, link); - } - else { - return true; - } - }) - .forEach((link) => { - this.preload[link.nodeId] = this.datasource.loadNode(link.nodeId) - .then((linkNode) => { - return this.psv.textureLoader.preloadPanorama(linkNode.panorama); - }) - .then(() => { - this.preload[link.nodeId] = true; - }) - .catch(() => { - delete this.preload[link.nodeId]; - }); - }); - } - -} diff --git a/src/plugins/virtual-tour/style.scss b/src/plugins/virtual-tour/style.scss deleted file mode 100644 index 4d29aa0f7..000000000 --- a/src/plugins/virtual-tour/style.scss +++ /dev/null @@ -1,24 +0,0 @@ -.psv-virtual-tour { - &__marker { - svg { - filter: drop-shadow(0 10px 5px rgba(0, 0, 0, .8)); - transform: perspective(100px) rotate3d(1, 0, 0, 0deg); - transform-origin: bottom center; - transition: .2s all ease-in-out; - } - - &:hover { - svg { - filter: drop-shadow(0 5px 3px rgba(0, 0, 0, 1)); - transform: perspective(100px) rotate3d(1, 0, 0, 10deg); - } - } - } - - &__menu { - .psv-panel-menu-item-icon { - width: 60px; - height: 60px; - } - } -} diff --git a/src/plugins/virtual-tour/utils.js b/src/plugins/virtual-tour/utils.js deleted file mode 100644 index afa212ba2..000000000 --- a/src/plugins/virtual-tour/utils.js +++ /dev/null @@ -1,76 +0,0 @@ -import { PSVError, utils } from 'photo-sphere-viewer'; - -/** - * @summary Checks the configuration of a node - * @param {PSV.plugins.VirtualTourPlugin.Node} node - * @param {boolean} isGps - * @private - */ -export function checkNode(node, isGps) { - if (!node.id) { - throw new PSVError('No id given for node'); - } - if (!node.panorama) { - throw new PSVError(`No panorama provided for node ${node.id}`); - } - if (isGps && !(node.position?.length >= 2)) { - throw new PSVError(`No position provided for node ${node.id}`); - } -} - -/** - * @summary Checks the configuration of a link - * @param {PSV.plugins.VirtualTourPlugin.Node} node - * @param {PSV.plugins.VirtualTourPlugin.NodeLink} link - * @param {boolean} isGps - * @private - */ -export function checkLink(node, link, isGps) { - if (!link.nodeId) { - throw new PSVError(`Link of node ${node.id} has no target id`); - } - if (!isGps && !utils.isExtendedPosition(link)) { - throw new PSVError(`No position provided for link ${link.nodeId} of node ${node.id}`); - } - if (isGps && !link.position) { - throw new PSVError(`No GPS position provided for link ${link.nodeId} of node ${node.id}`); - } -} - -/** - * @summary Changes the color of a mesh - * @param {external:THREE.Mesh} mesh - * @param {*} color - * @private - */ -export function setMeshColor(mesh, color) { - mesh.material.color.set(color); -} - -/** - * @summary Returns the distance between two GPS points - * @param {number[]} p1 - * @param {number[]} p2 - * @return {number} - * @private - */ -export function distance(p1, p2) { - return utils.greatArcDistance(p1, p2) * 6371e3; -} - -/** - * @summary Returns the bearing between two GPS points - * {@link http://www.movable-type.co.uk/scripts/latlong.html} - * @param {number[]} p1 - * @param {number[]} p2 - * @return {number} - * @private - */ -export function bearing(p1, p2) { - const [λ1, φ1] = p1; - const [λ2, φ2] = p2; - - const y = Math.sin(λ2 - λ1) * Math.cos(φ2); - const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1); - return Math.atan2(y, x); -} diff --git a/src/plugins/visible-range/index.js b/src/plugins/visible-range/index.js deleted file mode 100644 index dcaf892a3..000000000 --- a/src/plugins/visible-range/index.js +++ /dev/null @@ -1,297 +0,0 @@ -import { MathUtils } from 'three'; -import { AbstractPlugin, CONSTANTS, utils } from '../..'; - - -/** - * @typedef {Object} PSV.plugins.VisibleRangePlugin.Options - * @property {double[]|string[]} [latitudeRange] - latitude range as two angles - * @property {double[]|string[]} [longitudeRange] - longitude range as two angles - * @property {boolean} [usePanoData=false] - use panoData as visible range, you can also manually call `setRangesFromPanoData` - */ - -const EPS = 0.000001; - -/** - * @summary Locks visible longitude and/or latitude - * @extends PSV.plugins.AbstractPlugin - * @memberof PSV.plugins - */ -export class VisibleRangePlugin extends AbstractPlugin { - - static id = 'visible-range'; - - /** - * @param {PSV.Viewer} psv - * @param {PSV.plugins.VisibleRangePlugin.Options} options - */ - constructor(psv, options) { - super(psv); - - /** - * @member {PSV.plugins.VisibleRangePlugin.Options} - * @private - */ - this.config = { - latitudeRange : null, - longitudeRange: null, - usePanoData : false, - ...options, - }; - } - - /** - * @package - */ - init() { - super.init(); - - this.psv.on(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.psv.on(CONSTANTS.EVENTS.POSITION_UPDATED, this); - this.psv.on(CONSTANTS.EVENTS.ZOOM_UPDATED, this); - this.psv.on(CONSTANTS.CHANGE_EVENTS.GET_ANIMATE_POSITION, this); - this.psv.on(CONSTANTS.CHANGE_EVENTS.GET_ROTATE_POSITION, this); - - this.setLatitudeRange(this.config.latitudeRange); - this.setLongitudeRange(this.config.longitudeRange); - } - - /** - * @package - */ - destroy() { - this.psv.off(CONSTANTS.EVENTS.PANORAMA_LOADED, this); - this.psv.off(CONSTANTS.EVENTS.POSITION_UPDATED, this); - this.psv.off(CONSTANTS.EVENTS.ZOOM_UPDATED, this); - this.psv.off(CONSTANTS.CHANGE_EVENTS.GET_ANIMATE_POSITION, this); - this.psv.off(CONSTANTS.CHANGE_EVENTS.GET_ROTATE_POSITION, this); - - super.destroy(); - } - - /** - * @private - */ - // eslint-disable-next-line consistent-return - handleEvent(e) { - let sidesReached; - let rangedPosition; - let currentPosition; - - switch (e.type) { - case CONSTANTS.CHANGE_EVENTS.GET_ANIMATE_POSITION: - case CONSTANTS.CHANGE_EVENTS.GET_ROTATE_POSITION: - currentPosition = e.value; - ({ rangedPosition } = this.applyRanges(currentPosition)); - - return rangedPosition; - - case CONSTANTS.EVENTS.POSITION_UPDATED: - currentPosition = e.args[0]; - ({ sidesReached, rangedPosition } = this.applyRanges(currentPosition)); - - if ((sidesReached.left || sidesReached.right) && this.psv.isAutorotateEnabled()) { - this.__reverseAutorotate(sidesReached.left, sidesReached.right); - } - else if (Math.abs(currentPosition.longitude - rangedPosition.longitude) > EPS - || Math.abs(currentPosition.latitude - rangedPosition.latitude) > EPS) { - this.psv.dynamics.position.setValue(rangedPosition); - } - break; - - case CONSTANTS.EVENTS.PANORAMA_LOADED: - if (this.config.usePanoData) { - this.setRangesFromPanoData(); - } - break; - - case CONSTANTS.EVENTS.ZOOM_UPDATED: - currentPosition = this.psv.getPosition(); - ({ rangedPosition } = this.applyRanges(currentPosition)); - - if (Math.abs(currentPosition.longitude - rangedPosition.longitude) > EPS - || Math.abs(currentPosition.latitude - rangedPosition.latitude) > EPS) { - this.psv.rotate(rangedPosition); - } - break; - - default: - } - } - - /** - * @summary Changes the latitude range - * @param {double[]|string[]} range - latitude range as two angles - */ - setLatitudeRange(range) { - // latitude range must have two values - if (range && range.length !== 2) { - utils.logWarn('latitude range must have exactly two elements'); - range = null; - } - // latitude range must be ordered - else if (range && range[0] > range[1]) { - utils.logWarn('latitude range values must be ordered'); - range = [range[1], range[0]]; - } - // latitude range is between -PI/2 and PI/2 - if (range) { - this.config.latitudeRange = range.map(angle => utils.parseAngle(angle, true)); - } - else { - this.config.latitudeRange = null; - } - - if (this.psv.prop.ready) { - this.psv.rotate(this.psv.getPosition()); - } - } - - /** - * @summary Changes the longitude range - * @param {double[]|string[]} range - longitude range as two angles - */ - setLongitudeRange(range) { - // longitude range must have two values - if (range && range.length !== 2) { - utils.logWarn('longitude range must have exactly two elements'); - range = null; - } - // longitude range is between 0 and 2*PI - if (range) { - this.config.longitudeRange = range.map(angle => utils.parseAngle(angle)); - } - else { - this.config.longitudeRange = null; - } - - if (this.psv.prop.ready) { - this.psv.rotate(this.psv.getPosition()); - } - } - - /** - * @summary Changes the latitude and longitude ranges according the current panorama cropping data - */ - setRangesFromPanoData() { - this.setLatitudeRange(this.getPanoLatitudeRange()); - this.setLongitudeRange(this.getPanoLongitudeRange()); - } - - /** - * @summary Gets the latitude range defined by the viewer's panoData - * @returns {double[]|null} - * @private - */ - getPanoLatitudeRange() { - const p = this.psv.prop.panoData; - if (p.croppedHeight === p.fullHeight) { - return null; - } - else { - const latitude = y => Math.PI * (1 - y / p.fullHeight) - (Math.PI / 2); - return [latitude(p.croppedY + p.croppedHeight), latitude(p.croppedY)]; - } - } - - /** - * @summary Gets the longitude range defined by the viewer's panoData - * @returns {double[]|null} - * @private - */ - getPanoLongitudeRange() { - const p = this.psv.prop.panoData; - if (p.croppedWidth === p.fullWidth) { - return null; - } - else { - const longitude = x => 2 * Math.PI * (x / p.fullWidth) - Math.PI; - return [longitude(p.croppedX), longitude(p.croppedX + p.croppedWidth)]; - } - } - - /** - * @summary Apply "longitudeRange" and "latitudeRange" - * @param {PSV.Position} position - * @returns {{rangedPosition: PSV.Position, sidesReached: string[]}} - * @private - */ - applyRanges(position) { - const rangedPosition = { - longitude: position.longitude, - latitude : position.latitude, - }; - const sidesReached = {}; - - let range; - let offset; - - if (this.config.longitudeRange) { - range = utils.clone(this.config.longitudeRange); - offset = MathUtils.degToRad(this.psv.prop.hFov) / 2; - - range[0] = utils.parseAngle(range[0] + offset); - range[1] = utils.parseAngle(range[1] - offset); - - if (range[0] > range[1]) { // when the range cross longitude 0 - if (position.longitude > range[1] && position.longitude < range[0]) { - if (position.longitude > (range[0] / 2 + range[1] / 2)) { // detect which side we are closer too - rangedPosition.longitude = range[0]; - sidesReached.left = true; - } - else { - rangedPosition.longitude = range[1]; - sidesReached.right = true; - } - } - } - else if (position.longitude < range[0]) { - rangedPosition.longitude = range[0]; - sidesReached.left = true; - } - else if (position.longitude > range[1]) { - rangedPosition.longitude = range[1]; - sidesReached.right = true; - } - } - - if (this.config.latitudeRange) { - range = utils.clone(this.config.latitudeRange); - offset = MathUtils.degToRad(this.psv.prop.vFov) / 2; - - range[0] = utils.parseAngle(range[0] + offset, true); - range[1] = utils.parseAngle(range[1] - offset, true); - - // for very a narrow images, lock the latitude to the center - if (range[0] > range[1]) { - range[0] = (range[0] + range[1]) / 2; - range[1] = range[0]; - } - - if (position.latitude < range[0]) { - rangedPosition.latitude = range[0]; - sidesReached.bottom = true; - } - else if (position.latitude > range[1]) { - rangedPosition.latitude = range[1]; - sidesReached.top = true; - } - } - - return { rangedPosition, sidesReached }; - } - - /** - * @summary Reverses autorotate direction with smooth transition - * @private - */ - __reverseAutorotate(left, right) { - // reverse already ongoing - if (left && this.psv.config.autorotateSpeed > 0 || right && this.psv.config.autorotateSpeed < 0) { - return; - } - - this.psv.config.autorotateSpeed = -this.psv.config.autorotateSpeed; - this.psv.startAutorotate(true); - } - -} diff --git a/src/services/AbstractService.js b/src/services/AbstractService.js deleted file mode 100644 index 6db2819bf..000000000 --- a/src/services/AbstractService.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @namespace PSV.services - */ - -/** - * @summary Base services class - * @memberof PSV.services - * @abstract - */ -export class AbstractService { - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - /** - * @summary Reference to main controller - * @type {PSV.Viewer} - * @readonly - */ - this.psv = psv; - - /** - * @summary Configuration holder - * @type {PSV.Options} - * @readonly - */ - this.config = psv.config; - - /** - * @summary Properties holder - * @type {Object} - * @readonly - */ - this.prop = psv.prop; - } - - /** - * @summary Destroys the service - */ - destroy() { - delete this.psv; - delete this.config; - delete this.prop; - } - -} diff --git a/src/services/DataHelper.js b/src/services/DataHelper.js deleted file mode 100644 index ce05393c7..000000000 --- a/src/services/DataHelper.js +++ /dev/null @@ -1,261 +0,0 @@ -import { Euler, MathUtils, Vector2, Vector3 } from 'three'; -import { MESH_USER_DATA, SPHERE_RADIUS } from '../data/constants'; -import { PSVError } from '../PSVError'; -import { applyEulerInverse, parseAngle, parseSpeed } from '../utils'; -import { AbstractService } from './AbstractService'; - -const vector2 = new Vector2(); -const vector3 = new Vector3(); -const eulerZero = new Euler(0, 0, 0, 'ZXY'); - -/** - * @summary Collections of data converters for the current viewer - * @extends PSV.services.AbstractService - * @memberof PSV.services - */ -export class DataHelper extends AbstractService { - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv); - } - - /** - * @summary Converts vertical FOV to zoom level - * @param {number} fov - * @returns {number} - */ - fovToZoomLevel(fov) { - const temp = Math.round((fov - this.config.minFov) / (this.config.maxFov - this.config.minFov) * 100); - return temp - 2 * (temp - 50); - } - - /** - * @summary Converts zoom level to vertical FOV - * @param {number} level - * @returns {number} - */ - zoomLevelToFov(level) { - return this.config.maxFov + (level / 100) * (this.config.minFov - this.config.maxFov); - } - - /** - * @summary Convert vertical FOV to horizontal FOV - * @param {number} vFov - * @returns {number} - */ - vFovToHFov(vFov) { - return MathUtils.radToDeg(2 * Math.atan(Math.tan(MathUtils.degToRad(vFov) / 2) * this.prop.aspect)); - } - - /** - * @summary Converts a speed into a duration from current position to a new position - * @param {string|number} value - * @param {number} angle - * @returns {number} - */ - speedToDuration(value, angle) { - if (!value || typeof value !== 'number') { - // desired radial speed - const speed = value ? parseSpeed(value) : this.config.autorotateSpeed; - // compute duration - return angle / Math.abs(speed) * 1000; - } - else { - return Math.abs(value); - } - } - - /** - * @summary Converts pixel texture coordinates to spherical radians coordinates - * @param {PSV.Point} point - * @returns {PSV.Position} - * @throws {PSV.PSVError} when the current adapter does not support texture coordinates - */ - textureCoordsToSphericalCoords(point) { - const panoData = this.prop.panoData; - if (!panoData) { - throw new PSVError('Current adapter does not support texture coordinates.'); - } - - const relativeX = (point.x + panoData.croppedX) / panoData.fullWidth * Math.PI * 2; - const relativeY = (point.y + panoData.croppedY) / panoData.fullHeight * Math.PI; - - const result = { - longitude: relativeX >= Math.PI ? relativeX - Math.PI : relativeX + Math.PI, - latitude : Math.PI / 2 - relativeY, - }; - - // Apply panoData pose and sphereCorrection - if (!eulerZero.equals(this.psv.renderer.mesh.rotation) || !eulerZero.equals(this.psv.renderer.meshContainer.rotation)) { - this.sphericalCoordsToVector3(result, vector3); - vector3.applyEuler(this.psv.renderer.mesh.rotation); - vector3.applyEuler(this.psv.renderer.meshContainer.rotation); - return this.vector3ToSphericalCoords(vector3); - } - else { - return result; - } - } - - /** - * @summary Converts spherical radians coordinates to pixel texture coordinates - * @param {PSV.Position} position - * @returns {PSV.Point} - * @throws {PSV.PSVError} when the current adapter does not support texture coordinates - */ - sphericalCoordsToTextureCoords(position) { - const panoData = this.prop.panoData; - if (!panoData) { - throw new PSVError('Current adapter does not support texture coordinates.'); - } - - // Apply panoData pose and sphereCorrection - if (!eulerZero.equals(this.psv.renderer.mesh.rotation) || !eulerZero.equals(this.psv.renderer.meshContainer.rotation)) { - this.sphericalCoordsToVector3(position, vector3); - applyEulerInverse(vector3, this.psv.renderer.meshContainer.rotation); - applyEulerInverse(vector3, this.psv.renderer.mesh.rotation); - position = this.vector3ToSphericalCoords(vector3); - } - - const relativeLong = position.longitude / Math.PI / 2 * panoData.fullWidth; - const relativeLat = position.latitude / Math.PI * panoData.fullHeight; - - return { - x: Math.round(position.longitude < Math.PI ? relativeLong + panoData.fullWidth / 2 : relativeLong - panoData.fullWidth / 2) - panoData.croppedX, - y: Math.round(panoData.fullHeight / 2 - relativeLat) - panoData.croppedY, - }; - } - - /** - * @summary Converts spherical radians coordinates to a THREE.Vector3 - * @param {PSV.Position} position - * @param {external:THREE.Vector3} [vector] - * @returns {external:THREE.Vector3} - */ - sphericalCoordsToVector3(position, vector) { - if (!vector) { - vector = new Vector3(); - } - vector.x = SPHERE_RADIUS * -Math.cos(position.latitude) * Math.sin(position.longitude); - vector.y = SPHERE_RADIUS * Math.sin(position.latitude); - vector.z = SPHERE_RADIUS * Math.cos(position.latitude) * Math.cos(position.longitude); - return vector; - } - - /** - * @summary Converts a THREE.Vector3 to spherical radians coordinates - * @param {external:THREE.Vector3} vector - * @returns {PSV.Position} - */ - vector3ToSphericalCoords(vector) { - const phi = Math.acos(vector.y / Math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z)); - const theta = Math.atan2(vector.x, vector.z); - - return { - longitude: theta < 0 ? -theta : Math.PI * 2 - theta, - latitude : Math.PI / 2 - phi, - }; - } - - /** - * @summary Converts position on the viewer to a THREE.Vector3 - * @param {PSV.Point} viewerPoint - * @returns {external:THREE.Vector3} - */ - viewerCoordsToVector3(viewerPoint) { - const sphereIntersect = this.getIntersections(viewerPoint).filter(i => i.object.userData[MESH_USER_DATA]); - - if (sphereIntersect.length) { - return sphereIntersect[0].point; - } - else { - return null; - } - } - - /** - * @summary Converts a THREE.Vector3 to position on the viewer - * @param {external:THREE.Vector3} vector - * @returns {PSV.Point} - */ - vector3ToViewerCoords(vector) { - const vectorClone = vector.clone(); - vectorClone.project(this.psv.renderer.camera); - - return { - x: Math.round((vectorClone.x + 1) / 2 * this.prop.size.width), - y: Math.round((1 - vectorClone.y) / 2 * this.prop.size.height), - }; - } - - /** - * @summary Converts spherical radians coordinates to position on the viewer - * @param {PSV.Position} position - * @returns {PSV.Point} - */ - sphericalCoordsToViewerCoords(position) { - return this.vector3ToViewerCoords(this.sphericalCoordsToVector3(position, vector3)); - } - - /** - * @summary Returns intersections with objects in the scene - * @param {PSV.Point} viewerPoint - * @return {external:THREE.Intersection[]} - */ - getIntersections(viewerPoint) { - vector2.x = 2 * viewerPoint.x / this.prop.size.width - 1; - vector2.y = -2 * viewerPoint.y / this.prop.size.height + 1; - - this.psv.renderer.raycaster.setFromCamera(vector2, this.psv.renderer.camera); - - return this.psv.renderer.raycaster.intersectObjects(this.psv.renderer.scene.children, true) - .filter(i => !!i.object.userData); - } - - /** - * @summary Converts x/y to latitude/longitude if present and ensure boundaries - * @param {PSV.ExtendedPosition} position - * @returns {PSV.Position} - */ - cleanPosition(position) { - if (position.x !== undefined && position.y !== undefined) { - return this.textureCoordsToSphericalCoords(position); - } - else { - return { - longitude: parseAngle(position.longitude), - latitude : parseAngle(position.latitude, !this.prop.littlePlanet), - }; - } - } - - /** - * @summary Ensure a SphereCorrection object is valid - * @param {PSV.SphereCorrection} sphereCorrection - * @returns {PSV.SphereCorrection} - */ - cleanSphereCorrection(sphereCorrection) { - return { - pan : parseAngle(sphereCorrection?.pan || 0), - tilt: parseAngle(sphereCorrection?.tilt || 0, true), - roll: parseAngle(sphereCorrection?.roll || 0, true, false), - }; - } - - /** - * @summary Parse the pose angles of the pano data - * @param {PSV.PanoData} panoData - * @returns {PSV.SphereCorrection} - */ - cleanPanoramaPose(panoData) { - return { - pan : MathUtils.degToRad(panoData?.poseHeading || 0), - tilt: MathUtils.degToRad(panoData?.posePitch || 0), - roll: MathUtils.degToRad(panoData?.poseRoll || 0), - }; - } - -} diff --git a/src/services/EventsHandler.js b/src/services/EventsHandler.js deleted file mode 100644 index 9425eb217..000000000 --- a/src/services/EventsHandler.js +++ /dev/null @@ -1,818 +0,0 @@ -import { MathUtils, SplineCurve, Vector2 } from 'three'; -import { - ACTIONS, - CTRLZOOM_TIMEOUT, - DBLCLICK_DELAY, - EVENTS, - IDS, - INERTIA_WINDOW, - KEY_CODES, - LONGTOUCH_DELAY, - MESH_USER_DATA, - MOVE_THRESHOLD, - OBJECT_EVENTS, - TWOFINGERSOVERLAY_DELAY -} from '../data/constants'; -import { SYSTEM } from '../data/system'; -import gestureIcon from '../icons/gesture.svg'; -import mousewheelIcon from '../icons/mousewheel.svg'; -import { - clone, - distance, - each, - getClosest, - getPosition, - hasParent, - isEmpty, - isFullscreenEnabled, - normalizeWheel, - throttle -} from '../utils'; -import { Animation } from '../utils/Animation'; -import { PressHandler } from '../utils/PressHandler'; -import { AbstractService } from './AbstractService'; - -const IDLE = 0; -const MOVING = 1; -const INERTIA = 2; - -/** - * @summary Events handler - * @extends PSV.services.AbstractService - * @memberof PSV.services - */ -export class EventsHandler extends AbstractService { - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv); - - /** - * @summary Internal properties - * @member {Object} - * @property {number} moveThreshold - computed threshold based on device pixel ratio - * @property {number} step - * @property {boolean} mousedown - before moving past the threshold - * @property {number} startMouseX - start x position of the click/touch - * @property {number} startMouseY - start y position of the click/touch - * @property {number} mouseX - current x position of the cursor - * @property {number} mouseY - current y position of the cursor - * @property {number[][]} mouseHistory - list of latest positions of the cursor, [time, x, y] - * @property {number} pinchDist - distance between fingers when zooming - * @property {PressHandler} keyHandler - * @property {boolean} ctrlKeyDown - when the Ctrl key is pressed - * @property {PSV.ClickData} dblclickData - temporary storage of click data between two clicks - * @property {number} dblclickTimeout - timeout id for double click - * @property {number} twofingersTimeout - timeout id for "two fingers" overlay - * @property {number} ctrlZoomTimeout - timeout id for "ctrol zoom" overlay - * @protected - */ - this.state = { - moveThreshold : MOVE_THRESHOLD * SYSTEM.pixelRatio, - keyboardEnabled : false, - step : IDLE, - mousedown : false, - startMouseX : 0, - startMouseY : 0, - mouseX : 0, - mouseY : 0, - mouseHistory : [], - pinchDist : 0, - keyHandler : new PressHandler(), - ctrlKeyDown : false, - dblclickData : null, - dblclickTimeout : null, - longtouchTimeout : null, - twofingersTimeout: null, - ctrlZoomTimeout : null, - }; - - /** - * @summary Throttled wrapper of {@link PSV.Viewer#autoSize} - * @type {Function} - * @private - */ - this.__onResize = throttle(() => this.psv.autoSize(), 50); - } - - /** - * @summary Initializes event handlers - * @protected - */ - init() { - window.addEventListener('resize', this); - window.addEventListener('keydown', this, { passive: false }); - window.addEventListener('keyup', this); - this.psv.container.addEventListener('mousedown', this); - window.addEventListener('mousemove', this, { passive: false }); - window.addEventListener('mouseup', this); - this.psv.container.addEventListener('touchstart', this, { passive: false }); - window.addEventListener('touchmove', this, { passive: false }); - window.addEventListener('touchend', this, { passive: false }); - this.psv.container.addEventListener(SYSTEM.mouseWheelEvent, this, { passive: false }); - - if (SYSTEM.fullscreenEvent) { - document.addEventListener(SYSTEM.fullscreenEvent, this); - } - } - - /** - * @override - */ - destroy() { - window.removeEventListener('resize', this); - window.removeEventListener('keydown', this); - window.removeEventListener('keyup', this); - this.psv.container.removeEventListener('mousedown', this); - window.removeEventListener('mousemove', this); - window.removeEventListener('mouseup', this); - this.psv.container.removeEventListener('touchstart', this); - window.removeEventListener('touchmove', this); - window.removeEventListener('touchend', this); - this.psv.container.removeEventListener(SYSTEM.mouseWheelEvent, this); - - if (SYSTEM.fullscreenEvent) { - document.removeEventListener(SYSTEM.fullscreenEvent, this); - } - - clearTimeout(this.state.dblclickTimeout); - clearTimeout(this.state.longtouchTimeout); - clearTimeout(this.state.twofingersTimeout); - clearTimeout(this.state.ctrlZoomTimeout); - - delete this.state; - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} evt - * @private - */ - handleEvent(evt) { - /* eslint-disable */ - switch (evt.type) { - // @formatter:off - case 'resize': this.__onResize(); break; - case 'keydown': this.__onKeyDown(evt); break; - case 'keyup': this.__onKeyUp(); break; - case 'mousemove': this.__onMouseMove(evt); break; - case 'mouseup': this.__onMouseUp(evt); break; - case 'touchmove': this.__onTouchMove(evt); break; - case 'touchend': this.__onTouchEnd(evt); break; - case SYSTEM.fullscreenEvent: this.__fullscreenToggled(); break; - // @formatter:on - } - /* eslint-enable */ - - if (!getClosest(evt.target, '.psv--capture-event')) { - /* eslint-disable */ - switch (evt.type) { - // @formatter:off - case 'mousedown': this.__onMouseDown(evt); break; - case 'touchstart': this.__onTouchStart(evt); break; - case SYSTEM.mouseWheelEvent: this.__onMouseWheel(evt); break; - // @formatter:on - } - /* eslint-enable */ - } - } - - /** - * @summary Enables the keyboard controls - * @protected - */ - enableKeyboard() { - this.state.keyboardEnabled = true; - } - - /** - * @summary Disables the keyboard controls - * @protected - */ - disableKeyboard() { - this.state.keyboardEnabled = false; - } - - /** - * @summary Handles keyboard events - * @param {KeyboardEvent} e - * @private - */ - __onKeyDown(e) { - if (this.config.mousewheelCtrlKey) { - this.state.ctrlKeyDown = e.key === KEY_CODES.Control; - - if (this.state.ctrlKeyDown) { - clearTimeout(this.state.ctrlZoomTimeout); - this.psv.overlay.hide(IDS.CTRL_ZOOM); - } - } - - const e2 = this.psv.trigger(EVENTS.KEY_PRESS, e.key); - if (e2.isDefaultPrevented()) { - return; - } - - if (!this.state.keyboardEnabled) { - return; - } - - const action = this.config.keyboard[e.key]; - if (action === ACTIONS.TOGGLE_AUTOROTATE) { - this.psv.toggleAutorotate(); - e.preventDefault(); - } - else if (action && !this.state.keyHandler.time) { - if (action !== ACTIONS.ZOOM_IN && action !== ACTIONS.ZOOM_OUT) { - this.psv.__stopAll(); - } - - /* eslint-disable */ - switch (action) { - // @formatter:off - case ACTIONS.ROTATE_LAT_UP: this.psv.dynamics.position.roll({latitude: false}); break; - case ACTIONS.ROTATE_LAT_DOWN: this.psv.dynamics.position.roll({latitude: true}); break; - case ACTIONS.ROTATE_LONG_RIGHT: this.psv.dynamics.position.roll({longitude: false}); break; - case ACTIONS.ROTATE_LONG_LEFT: this.psv.dynamics.position.roll({longitude: true}); break; - case ACTIONS.ZOOM_IN: this.psv.dynamics.zoom.roll(false); break; - case ACTIONS.ZOOM_OUT: this.psv.dynamics.zoom.roll(true); break; - // @formatter:on - } - /* eslint-enable */ - - this.state.keyHandler.down(); - e.preventDefault(); - } - } - - /** - * @summary Handles keyboard events - * @private - */ - __onKeyUp() { - this.state.ctrlKeyDown = false; - - if (!this.state.keyboardEnabled) { - return; - } - - this.state.keyHandler.up(() => { - this.psv.dynamics.position.stop(); - this.psv.dynamics.zoom.stop(); - this.psv.resetIdleTimer(); - }); - } - - /** - * @summary Handles mouse down events - * @param {MouseEvent} evt - * @private - */ - __onMouseDown(evt) { - this.state.mousedown = true; - this.state.startMouseX = evt.clientX; - this.state.startMouseY = evt.clientY; - } - - /** - * @summary Handles mouse up events - * @param {MouseEvent} evt - * @private - */ - __onMouseUp(evt) { - if (this.state.mousedown || this.state.step === MOVING) { - this.__stopMove(evt.clientX, evt.clientY, evt.target, evt.button === 2); - } - } - - /** - * @summary Handles mouse move events - * @param {MouseEvent} evt - * @private - */ - __onMouseMove(evt) { - if (this.config.mousemove && (this.state.mousedown || this.state.step === MOVING)) { - evt.preventDefault(); - this.__move(evt.clientX, evt.clientY); - } - - if (!isEmpty(this.prop.objectsObservers) && hasParent(evt.target, this.psv.container)) { - const viewerPos = getPosition(this.psv.container); - - const viewerPoint = { - x: evt.clientX - viewerPos.left, - y: evt.clientY - viewerPos.top, - }; - - const intersections = this.psv.dataHelper.getIntersections(viewerPoint); - - const emit = (observer, key, type) => { - observer.listener.handleEvent(new CustomEvent(type, { - detail: { - originalEvent: evt, - object : observer.object, - data : observer.object.userData[key], - viewerPoint : viewerPoint, - }, - })); - }; - - each(this.prop.objectsObservers, (observer, key) => { - const intersection = intersections.find(i => i.object.userData[key]); - - if (intersection) { - if (observer.object && intersection.object !== observer.object) { - emit(observer, key, OBJECT_EVENTS.LEAVE_OBJECT); - delete observer.object; - } - - if (!observer.object) { - observer.object = intersection.object; - emit(observer, key, OBJECT_EVENTS.ENTER_OBJECT); - } - else { - emit(observer, key, OBJECT_EVENTS.HOVER_OBJECT); - } - } - else if (observer.object) { - emit(observer, key, OBJECT_EVENTS.LEAVE_OBJECT); - delete observer.object; - } - }); - } - } - - /** - * @summary Handles touch events - * @param {TouchEvent} evt - * @private - */ - __onTouchStart(evt) { - if (evt.touches.length === 1) { - this.state.mousedown = true; - this.state.startMouseX = evt.touches[0].clientX; - this.state.startMouseY = evt.touches[0].clientY; - - if (!this.prop.longtouchTimeout) { - this.prop.longtouchTimeout = setTimeout(() => { - const touch = evt.touches[0]; - this.__stopMove(touch.clientX, touch.clientY, touch.target, true); - this.prop.longtouchTimeout = null; - }, LONGTOUCH_DELAY); - } - } - else if (evt.touches.length === 2) { - this.state.mousedown = false; - this.__cancelLongTouch(); - - if (this.config.mousemove) { - this.__cancelTwoFingersOverlay(); - this.__startMoveZoom(evt); - evt.preventDefault(); - } - } - } - - /** - * @summary Handles touch events - * @param {TouchEvent} evt - * @private - */ - __onTouchEnd(evt) { - this.__cancelLongTouch(); - - if (this.state.mousedown || this.state.step === MOVING) { - evt.preventDefault(); - this.__cancelTwoFingersOverlay(); - - if (evt.touches.length === 1) { - this.__stopMove(this.state.mouseX, this.state.mouseY); - } - else if (evt.touches.length === 0) { - const touch = evt.changedTouches[0]; - this.__stopMove(touch.clientX, touch.clientY, touch.target); - } - } - } - - /** - * @summary Handles touch move events - * @param {TouchEvent} evt - * @private - */ - __onTouchMove(evt) { - this.__cancelLongTouch(); - - if (!this.config.mousemove) { - return; - } - - if (evt.touches.length === 1) { - if (this.config.touchmoveTwoFingers) { - if (this.state.mousedown && !this.prop.twofingersTimeout) { - this.prop.twofingersTimeout = setTimeout(() => { - this.psv.overlay.show({ - id : IDS.TWO_FINGERS, - image: gestureIcon, - text : this.config.lang.twoFingers, - }); - }, TWOFINGERSOVERLAY_DELAY); - } - } - else if (this.state.mousedown || this.state.step === MOVING) { - evt.preventDefault(); - const touch = evt.touches[0]; - this.__move(touch.clientX, touch.clientY); - } - } - else { - this.__moveZoom(evt); - this.__cancelTwoFingersOverlay(); - } - } - - /** - * @summary Cancel the long touch timer if any - * @private - */ - __cancelLongTouch() { - if (this.prop.longtouchTimeout) { - clearTimeout(this.prop.longtouchTimeout); - this.prop.longtouchTimeout = null; - } - } - - /** - * @summary Cancel the two fingers overlay timer if any - * @private - */ - __cancelTwoFingersOverlay() { - if (this.config.touchmoveTwoFingers) { - if (this.prop.twofingersTimeout) { - clearTimeout(this.prop.twofingersTimeout); - this.prop.twofingersTimeout = null; - } - this.psv.overlay.hide(IDS.TWO_FINGERS); - } - } - - /** - * @summary Handles mouse wheel events - * @param {WheelEvent} evt - * @private - */ - __onMouseWheel(evt) { - if (!this.config.mousewheel) { - return; - } - - if (this.config.mousewheelCtrlKey && !this.state.ctrlKeyDown) { - this.psv.overlay.show({ - id : IDS.CTRL_ZOOM, - image: mousewheelIcon, - text : this.config.lang.ctrlZoom, - }); - - clearTimeout(this.state.ctrlZoomTimeout); - this.state.ctrlZoomTimeout = setTimeout(() => this.psv.overlay.hide(IDS.CTRL_ZOOM), CTRLZOOM_TIMEOUT); - - return; - } - - evt.preventDefault(); - evt.stopPropagation(); - - const delta = normalizeWheel(evt).spinY * 5 * this.config.zoomSpeed; - if (delta !== 0) { - this.psv.dynamics.zoom.step(-delta, 5); - } - } - - /** - * @summary Handles fullscreen events - * @param {boolean} [force] force state - * @fires PSV.fullscreen-updated - * @package - */ - __fullscreenToggled(force) { - this.prop.fullscreen = force !== undefined ? force : isFullscreenEnabled(this.psv.container); - - if (this.config.keyboard) { - if (this.prop.fullscreen) { - this.psv.startKeyboardControl(); - } - else { - this.psv.stopKeyboardControl(); - } - } - - this.psv.trigger(EVENTS.FULLSCREEN_UPDATED, this.prop.fullscreen); - } - - /** - * @summary Resets all state variables - * @private - */ - __resetMove() { - this.state.step = IDLE; - this.state.mousedown = false; - this.state.mouseX = 0; - this.state.mouseY = 0; - this.state.startMouseX = 0; - this.state.startMouseY = 0; - this.state.mouseHistory.length = 0; - } - - /** - * @summary Initializes the combines move and zoom - * @param {TouchEvent} evt - * @private - */ - __startMoveZoom(evt) { - this.psv.__stopAll(); - this.__resetMove(); - - const p1 = { x: evt.touches[0].clientX, y: evt.touches[0].clientY }; - const p2 = { x: evt.touches[1].clientX, y: evt.touches[1].clientY }; - - this.state.step = MOVING; - this.state.pinchDist = distance(p1, p2); - this.state.mouseX = (p1.x + p2.x) / 2; - this.state.mouseY = (p1.y + p2.y) / 2; - this.__logMouseMove(this.state.mouseX, this.state.mouseY); - } - - /** - * @summary Stops the movement - * @description If the move threshold was not reached a click event is triggered, otherwise an animation is launched to simulate inertia - * @param {int} clientX - * @param {int} clientY - * @param {EventTarget} [target] - * @param {boolean} [rightclick=false] - * @private - */ - __stopMove(clientX, clientY, target = null, rightclick = false) { - if (this.state.step === MOVING) { - if (this.config.moveInertia) { - this.__logMouseMove(clientX, clientY); - this.__stopMoveInertia(clientX, clientY); - } - else { - this.__resetMove(); - this.psv.resetIdleTimer(); - } - } - else if (this.state.mousedown) { - this.psv.stopAnimation(); - this.__click(clientX, clientY, target, rightclick); - this.__resetMove(); - this.psv.resetIdleTimer(); - } - } - - /** - * @summary Performs an animation to simulate inertia when the movement stops - * @param {int} clientX - * @param {int} clientY - * @private - */ - __stopMoveInertia(clientX, clientY) { - // get direction at end of movement - const curve = new SplineCurve(this.state.mouseHistory.map(([, x, y]) => new Vector2(x, y))); - const direction = curve.getTangent(1); - - // average speed - const speed = this.state.mouseHistory.slice(1).reduce(({ total, prev }, curr) => { - return { - total: total + distance({ x: prev[1], y: prev[2] }, { x: curr[1], y: curr[2] }) / (curr[0] - prev[0]), - prev : curr, - }; - }, { - total: 0, - prev : this.state.mouseHistory[0], - }).total / this.state.mouseHistory.length; - - if (!speed) { - this.__resetMove(); - this.psv.resetIdleTimer(); - return; - } - - this.state.step = INERTIA; - - let currentClientX = clientX; - let currentClientY = clientY; - - this.prop.animationPromise = new Animation({ - properties: { - speed: { start: speed, end: 0 }, - }, - duration : 1000, - easing : 'outQuad', - onTick : (properties) => { - // 3 is a magic number - currentClientX += properties.speed * direction.x * 3 * SYSTEM.pixelRatio; - currentClientY += properties.speed * direction.y * 3 * SYSTEM.pixelRatio; - this.__applyMove(currentClientX, currentClientY); - }, - }); - - this.prop.animationPromise - .then((done) => { - this.prop.animationPromise = null; - if (done) { - this.__resetMove(); - this.psv.resetIdleTimer(); - } - }); - } - - /** - * @summary Triggers an event with all coordinates when a simple click is performed - * @param {int} clientX - * @param {int} clientY - * @param {EventTarget} target - * @param {boolean} [rightclick=false] - * @fires PSV.click - * @fires PSV.dblclick - * @private - */ - __click(clientX, clientY, target, rightclick = false) { - const boundingRect = this.psv.container.getBoundingClientRect(); - - /** - * @type {PSV.ClickData} - */ - const data = { - rightclick: rightclick, - target : target, - clientX : clientX, - clientY : clientY, - viewerX : clientX - boundingRect.left, - viewerY : clientY - boundingRect.top, - }; - - const intersections = this.psv.dataHelper.getIntersections({ - x: data.viewerX, - y: data.viewerY, - }); - - const sphereIntersection = intersections.find(i => i.object.userData[MESH_USER_DATA]); - - if (sphereIntersection) { - const sphericalCoords = this.psv.dataHelper.vector3ToSphericalCoords(sphereIntersection.point); - data.longitude = sphericalCoords.longitude; - data.latitude = sphericalCoords.latitude; - - data.objects = intersections.map(i => i.object).filter(o => !o.userData[MESH_USER_DATA]); - - try { - const textureCoords = this.psv.dataHelper.sphericalCoordsToTextureCoords(data); - data.textureX = textureCoords.x; - data.textureY = textureCoords.y; - } - catch (e) { - data.textureX = NaN; - data.textureY = NaN; - } - - if (!this.state.dblclickTimeout) { - this.psv.trigger(EVENTS.CLICK, data); - - this.state.dblclickData = clone(data); - this.state.dblclickTimeout = setTimeout(() => { - this.state.dblclickTimeout = null; - this.state.dblclickData = null; - }, DBLCLICK_DELAY); - } - else { - if (Math.abs(this.state.dblclickData.clientX - data.clientX) < this.state.moveThreshold - && Math.abs(this.state.dblclickData.clientY - data.clientY) < this.state.moveThreshold) { - this.psv.trigger(EVENTS.DOUBLE_CLICK, this.state.dblclickData); - } - - clearTimeout(this.state.dblclickTimeout); - this.state.dblclickTimeout = null; - this.state.dblclickData = null; - } - } - } - - /** - * @summary Starts moving when crossing moveThreshold and performs movement - * @param {int} clientX - * @param {int} clientY - * @private - */ - __move(clientX, clientY) { - if (this.state.mousedown - && (Math.abs(clientX - this.state.startMouseX) >= this.state.moveThreshold - || Math.abs(clientY - this.state.startMouseY) >= this.state.moveThreshold)) { - this.psv.__stopAll(); - this.__resetMove(); - this.state.step = MOVING; - this.state.mouseX = clientX; - this.state.mouseY = clientY; - this.__logMouseMove(clientX, clientY); - } - else if (this.state.step === MOVING) { - this.__applyMove(clientX, clientY); - this.__logMouseMove(clientX, clientY); - } - } - - /** - * @summary Raw method for movement, called from mouse event and move inertia - * @param {int} clientX - * @param {int} clientY - * @private - */ - __applyMove(clientX, clientY) { - const rotation = { - longitude: (clientX - this.state.mouseX) / this.prop.size.width * this.config.moveSpeed - * MathUtils.degToRad(this.prop.littlePlanet ? 90 : this.prop.hFov), - latitude : (clientY - this.state.mouseY) / this.prop.size.height * this.config.moveSpeed - * MathUtils.degToRad(this.prop.littlePlanet ? 90 : this.prop.vFov), - }; - - const currentPosition = this.psv.getPosition(); - this.psv.rotate({ - longitude: currentPosition.longitude - rotation.longitude, - latitude : currentPosition.latitude + rotation.latitude, - }); - - this.state.mouseX = clientX; - this.state.mouseY = clientY; - } - - /** - * @summary Perfoms combined move and zoom - * @param {TouchEvent} evt - * @private - */ - __moveZoom(evt) { - if (this.state.step === MOVING) { - evt.preventDefault(); - - const p1 = { x: evt.touches[0].clientX, y: evt.touches[0].clientY }; - const p2 = { x: evt.touches[1].clientX, y: evt.touches[1].clientY }; - - const p = distance(p1, p2); - const delta = (p - this.state.pinchDist) / SYSTEM.pixelRatio * this.config.zoomSpeed; - - this.psv.zoom(this.psv.getZoomLevel() + delta); - - this.__move((p1.x + p2.x) / 2, (p1.y + p2.y) / 2); - - this.state.pinchDist = p; - } - } - - /** - * @summary Stores each mouse position during a mouse move - * @description Positions older than "INERTIA_WINDOW" are removed
- * Positions before a pause of "INERTIA_WINDOW" / 10 are removed - * @param {int} clientX - * @param {int} clientY - * @private - */ - __logMouseMove(clientX, clientY) { - const now = Date.now(); - - const last = this.state.mouseHistory.length ? this.state.mouseHistory[this.state.mouseHistory.length - 1] : [0, -1, -1]; - - // avoid duplicates - if (last[1] === clientX && last[2] === clientY) { - last[0] = now; - } - else if (now === last[0]) { - last[1] = clientX; - last[2] = clientY; - } - else { - this.state.mouseHistory.push([now, clientX, clientY]); - } - - let previous = null; - - for (let i = 0; i < this.state.mouseHistory.length;) { - if (this.state.mouseHistory[i][0] < now - INERTIA_WINDOW) { - this.state.mouseHistory.splice(i, 1); - } - else if (previous && this.state.mouseHistory[i][0] - previous > INERTIA_WINDOW / 10) { - this.state.mouseHistory.splice(0, i); - i = 0; - previous = this.state.mouseHistory[i][0]; - } - else { - previous = this.state.mouseHistory[i][0]; - i++; - } - } - } - -} diff --git a/src/services/Renderer.js b/src/services/Renderer.js deleted file mode 100644 index 57bf81de0..000000000 --- a/src/services/Renderer.js +++ /dev/null @@ -1,419 +0,0 @@ -import { Group, PerspectiveCamera, Raycaster, Scene, Vector3, WebGLRenderer } from 'three'; -import { EVENTS, MESH_USER_DATA, SPHERE_RADIUS } from '../data/constants'; -import { SYSTEM } from '../data/system'; -import { Animation, each, isExtendedPosition } from '../utils'; -import { AbstractService } from './AbstractService'; - -/** - * @summary Viewer and renderer - * @extends PSV.services.AbstractService - * @memberof PSV.services - */ -export class Renderer extends AbstractService { - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv); - - /** - * @member {external:THREE.WebGLRenderer} - * @readonly - * @protected - */ - this.renderer = new WebGLRenderer({ alpha: true, antialias: true }); - this.renderer.setPixelRatio(SYSTEM.pixelRatio); - this.renderer.domElement.className = 'psv-canvas'; - - /** - * @member {external:THREE.Scene} - * @readonly - * @protected - */ - this.scene = new Scene(); - - /** - * @member {external:THREE.PerspectiveCamera} - * @readonly - * @protected - */ - this.camera = new PerspectiveCamera(50, 16 / 9, 0.1, 2 * SPHERE_RADIUS); - - /** - * @member {external:THREE.Mesh} - * @readonly - * @protected - */ - this.mesh = this.psv.adapter.createMesh(); - this.mesh.userData = { [MESH_USER_DATA]: true }; - - /** - * @member {external:THREE.Group} - * @readonly - * @private - */ - this.meshContainer = new Group(); - this.meshContainer.add(this.mesh); - this.scene.add(this.meshContainer); - - /** - * @member {external:THREE.Raycaster} - * @readonly - * @protected - */ - this.raycaster = new Raycaster(); - - /** - * @member {number} - * @private - */ - this.timestamp = null; - - /** - * @member {boolean} - * @private - */ - this.ready = false; - - /** - * @member {HTMLElement} - * @readonly - * @package - */ - this.canvasContainer = document.createElement('div'); - this.canvasContainer.className = 'psv-canvas-container'; - this.canvasContainer.style.background = this.psv.config.canvasBackground; - this.canvasContainer.style.cursor = this.psv.config.mousemove ? 'move' : 'default'; - this.canvasContainer.appendChild(this.renderer.domElement); - this.psv.container.appendChild(this.canvasContainer); - - psv.on(EVENTS.SIZE_UPDATED, this); - psv.on(EVENTS.ZOOM_UPDATED, this); - psv.on(EVENTS.POSITION_UPDATED, this); - psv.on(EVENTS.CONFIG_CHANGED, this); - - this.hide(); - } - - /** - * @override - */ - destroy() { - // cancel render loop - this.renderer.setAnimationLoop(null); - - // destroy ThreeJS view - this.__cleanTHREEScene(this.scene); - - // remove container - this.psv.container.removeChild(this.canvasContainer); - - delete this.canvasContainer; - delete this.renderer; - delete this.scene; - delete this.camera; - delete this.mesh; - delete this.meshContainer; - delete this.raycaster; - - super.destroy(); - } - - /** - * @summary Handles events - * @param {Event} evt - * @private - */ - handleEvent(evt) { - /* eslint-disable */ - switch (evt.type) { - // @formatter:off - case EVENTS.SIZE_UPDATED: this.__onSizeUpdated(); break; - case EVENTS.ZOOM_UPDATED: this.__onZoomUpdated(); break; - case EVENTS.POSITION_UPDATED: this.__onPositionUpdated(); break; - case EVENTS.CONFIG_CHANGED: - if (evt.args[0].includes('fisheye')) { - this.__onPositionUpdated(); - } - if (evt.args[0].includes('mousemove')) { - this.canvasContainer.style.cursor = this.psv.config.mousemove ? 'move' : 'default'; - } - break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @summary Hides the viewer - */ - hide() { - this.canvasContainer.style.opacity = 0; - } - - /** - * @summary Shows the viewer - */ - show() { - this.canvasContainer.style.opacity = 1; - } - - /** - * @summary Updates the size of the renderer and the aspect of the camera - * @private - */ - __onSizeUpdated() { - this.renderer.setSize(this.prop.size.width, this.prop.size.height); - this.camera.aspect = this.prop.aspect; - this.camera.updateProjectionMatrix(); - this.prop.needsUpdate = true; - } - - /** - * @summary Updates the fov of the camera - * @private - */ - __onZoomUpdated() { - this.camera.fov = this.prop.vFov; - this.camera.updateProjectionMatrix(); - this.prop.needsUpdate = true; - } - - /** - * @summary Updates the position of the camera - * @private - */ - __onPositionUpdated() { - this.camera.position.set(0, 0, 0); - this.camera.lookAt(this.prop.direction); - if (this.config.fisheye) { - this.camera.position.copy(this.prop.direction).multiplyScalar(this.config.fisheye / 2).negate(); - } - this.prop.needsUpdate = true; - } - - /** - * @summary Main event loop, calls {@link render} if `prop.needsUpdate` is true - * @param {number} timestamp - * @fires PSV.before-render - * @private - */ - __renderLoop(timestamp) { - const elapsed = this.timestamp !== null ? timestamp - this.timestamp : 0; - this.timestamp = timestamp; - - this.psv.trigger(EVENTS.BEFORE_RENDER, timestamp, elapsed); - each(this.psv.dynamics, d => d.update(elapsed)); - - if (this.prop.idleTime > 0 && timestamp - this.prop.idleTime > this.config.autorotateDelay) { - this.psv.startAutorotate(); - } - - if (this.prop.needsUpdate) { - this.render(); - this.prop.needsUpdate = false; - } - } - - /** - * @summary Performs a render - * @description Do not call this method directly, instead call - * {@link PSV.Viewer#needsUpdate} on {@link PSV.event:before-render}. - * @fires PSV.render - */ - render() { - this.renderer.render(this.scene, this.camera); - this.psv.trigger(EVENTS.RENDER); - } - - /** - * @summary Applies the texture to the scene, creates the scene if needed - * @param {PSV.TextureData} textureData - * @fires PSV.panorama-loaded - * @package - */ - setTexture(textureData) { - this.prop.panoData = textureData.panoData; - - this.psv.adapter.setTexture(this.mesh, textureData); - - if (!this.ready) { - this.renderer.setAnimationLoop(t => this.__renderLoop(t)); - this.ready = true; - } - - this.psv.needsUpdate(); - - this.psv.trigger(EVENTS.PANORAMA_LOADED, textureData); - } - - /** - * @summary Applies the overlay to the mesh - * @param {PSV.TextureData} textureData - * @param {number} opacity - * @package - */ - setOverlay(textureData, opacity) { - this.psv.adapter.setOverlay(this.mesh, textureData, opacity); - this.psv.needsUpdate(); - } - - /** - * @summary Apply a panorama data pose to a Mesh - * @param {PSV.PanoData} [panoData] - * @param {external:THREE.Mesh} [mesh=this.mesh] - * @package - */ - setPanoramaPose(panoData, mesh = this.mesh) { - // By Google documentation the angles are applied on the camera in order : heading, pitch, roll - // here we apply the reverse transformation on the sphere - const cleanCorrection = this.psv.dataHelper.cleanPanoramaPose(panoData); - - mesh.rotation.set( - -cleanCorrection.tilt, - -cleanCorrection.pan, - -cleanCorrection.roll, - 'ZXY' - ); - } - - /** - * @summary Apply a SphereCorrection to a Mesh - * @param {PSV.SphereCorrection} [sphereCorrection] - * @param {external:THREE.Mesh} [mesh=this.meshContainer] - * @package - */ - setSphereCorrection(sphereCorrection, mesh = this.meshContainer) { - const cleanCorrection = this.psv.dataHelper.cleanSphereCorrection(sphereCorrection); - - mesh.rotation.set( - cleanCorrection.tilt, - cleanCorrection.pan, - cleanCorrection.roll, - 'ZXY' - ); - } - - /** - * @summary Performs transition between the current and a new texture - * @param {PSV.TextureData} textureData - * @param {PSV.PanoramaOptions} options - * @returns {PSV.utils.Animation} - * @package - */ - transition(textureData, options) { - const positionProvided = isExtendedPosition(options); - const zoomProvided = 'zoom' in options; - - // create temp group and new mesh, half size to be in "front" of the first one - const group = new Group(); - const mesh = this.psv.adapter.createMesh(0.5); - this.psv.adapter.setTexture(mesh, textureData, true); - this.psv.adapter.setTextureOpacity(mesh, 0); - this.setPanoramaPose(textureData.panoData, mesh); - this.setSphereCorrection(options.sphereCorrection, group); - - // rotate the new sphere to make the target position face the camera - if (positionProvided) { - const cleanPosition = this.psv.dataHelper.cleanPosition(options); - const currentPosition = this.psv.getPosition(); - - // Longitude rotation along the vertical axis - const verticalAxis = new Vector3(0, 1, 0); - group.rotateOnWorldAxis(verticalAxis, cleanPosition.longitude - currentPosition.longitude); - - // Latitude rotation along the camera horizontal axis - const horizontalAxis = new Vector3(0, 1, 0).cross(this.camera.getWorldDirection(new Vector3())).normalize(); - group.rotateOnWorldAxis(horizontalAxis, cleanPosition.latitude - currentPosition.latitude); - } - - group.add(mesh); - this.scene.add(group); - - const animation = new Animation({ - properties: { - opacity: { start: 0.0, end: 1.0 }, - zoom : zoomProvided ? { start: this.psv.getZoomLevel(), end: options.zoom } : undefined, - }, - duration : options.transition, - easing : 'outCubic', - onTick : (properties) => { - this.psv.adapter.setTextureOpacity(mesh, properties.opacity); - this.psv.adapter.setTextureOpacity(this.mesh, 1 - properties.opacity); - - if (zoomProvided) { - this.psv.zoom(properties.zoom); - } - - this.psv.needsUpdate(); - }, - }); - - animation - .then((completed) => { - if (completed) { - // remove temp sphere and transfer the texture to the main mesh - this.setTexture(textureData); - this.psv.adapter.setTextureOpacity(this.mesh, 1); - this.setPanoramaPose(textureData.panoData); - this.setSphereCorrection(options.sphereCorrection); - - // actually rotate the camera - if (positionProvided) { - this.psv.rotate(options); - } - } - else { - this.psv.adapter.disposeTexture(textureData); - } - - this.scene.remove(group); - mesh.geometry.dispose(); - mesh.geometry = null; - }); - - return animation; - } - - /** - * @summary Calls `dispose` on all objects and textures - * @param {external:THREE.Object3D} object - * @private - */ - __cleanTHREEScene(object) { - object.traverse((item) => { - if (item.geometry) { - item.geometry.dispose(); - } - - if (item.material) { - if (Array.isArray(item.material)) { - item.material.forEach((material) => { - if (material.map) { - material.map.dispose(); - } - - material.dispose(); - }); - } - else { - if (item.material.map) { - item.material.map.dispose(); - } - - item.material.dispose(); - } - } - - if (item.dispose && !(item instanceof Scene)) { - item.dispose(); - } - - if (item !== object) { - this.__cleanTHREEScene(item); - } - }); - } - -} diff --git a/src/services/TextureLoader.js b/src/services/TextureLoader.js deleted file mode 100644 index d3fda5eb9..000000000 --- a/src/services/TextureLoader.js +++ /dev/null @@ -1,119 +0,0 @@ -import { FileLoader } from 'three'; -import { AbstractService } from './AbstractService'; - -/** - * @summary Texture loader - * @extends PSV.services.AbstractService - * @memberof PSV.services - */ -export class TextureLoader extends AbstractService { - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv); - - /** - * @summary THREE file loader - * @type {external:THREE:FileLoader} - * @private - */ - this.loader = new FileLoader(); - this.loader.setResponseType('blob'); - if (this.config.withCredentials) { - this.loader.setWithCredentials(true); - } - if (this.config.requestHeaders && typeof this.config.requestHeaders === 'object') { - this.loader.setRequestHeader(this.config.requestHeaders); - } - } - - /** - * @override - */ - destroy() { - this.abortLoading(); - super.destroy(); - } - - /** - * @summary Cancels current HTTP requests - * @package - */ - abortLoading() { - // noop implementation waiting for https://github.com/mrdoob/three.js/pull/23070 - } - - /** - * @summary Loads a Blob with FileLoader - * @param {string} url - * @param {function(number)} [onProgress] - * @returns {Promise} - */ - loadFile(url, onProgress) { - if (this.config.requestHeaders && typeof this.config.requestHeaders === 'function') { - this.loader.setRequestHeader(this.config.requestHeaders(url)); - } - - return new Promise((resolve, reject) => { - let progress = 0; - onProgress?.(progress); - - this.loader.load( - url, - (result) => { - progress = 100; - onProgress?.(progress); - resolve(result); - }, - (e) => { - if (e.lengthComputable) { - const newProgress = e.loaded / e.total * 100; - if (newProgress > progress) { - progress = newProgress; - onProgress?.(progress); - } - } - }, - (err) => { - reject(err); - } - ); - }); - } - - /** - * @summary Loads an Image using FileLoader to have progress events - * @param {string} url - * @param {function(number)} [onProgress] - * @returns {Promise} - */ - loadImage(url, onProgress) { - return this.loadFile(url, onProgress) - .then(result => new Promise((resolve, reject) => { - const img = document.createElementNS('http://www.w3.org/1999/xhtml', 'img'); - img.onload = () => { - URL.revokeObjectURL(img.src); - resolve(img); - }; - img.onerror = reject; - img.src = URL.createObjectURL(result); - })); - } - - /** - * @summary Preload a panorama file without displaying it - * @param {*} panorama - * @returns {Promise} - */ - preloadPanorama(panorama) { - if (this.psv.adapter.supportsPreload(panorama)) { - return this.psv.adapter.loadTexture(panorama); - } - else { - return Promise.resolve(); - } - } - -} diff --git a/src/services/TooltipRenderer.js b/src/services/TooltipRenderer.js deleted file mode 100644 index e06923d65..000000000 --- a/src/services/TooltipRenderer.js +++ /dev/null @@ -1,62 +0,0 @@ -import { Tooltip } from '../components/Tooltip'; -import { getStyle } from '../utils'; -import { AbstractService } from './AbstractService'; - -/** - * @summary Tooltip renderer - * @extends PSV.services.AbstractService - * @memberof PSV.services - */ -export class TooltipRenderer extends AbstractService { - - /** - * @param {PSV.Viewer} psv - */ - constructor(psv) { - super(psv); - - const testTooltip = new Tooltip(this.psv, { arrow: 0, border: 0 }); - - /** - * @summary Computed static sizes - * @member {Object} - * @package - * @property {number} arrow - * @property {number} border - */ - this.size = { - arrow : parseInt(getStyle(testTooltip.arrow, 'borderTopWidth'), 10), - border: parseInt(getStyle(testTooltip.container, 'borderTopLeftRadius'), 10), - }; - - testTooltip.destroy(); - } - - /** - * @override - */ - destroy() { - delete this.size; - - super.destroy(); - } - - /** - * @summary Displays a tooltip on the viewer - * @param {PSV.components.Tooltip.Config} config - * @returns {PSV.components.Tooltip} - * - * @fires PSV.show-tooltip - * @throws {PSV.PSVError} when the configuration is incorrect - * - * @example - * viewer.tooltip.create({ content: 'Hello world', top: 200, left: 450, position: 'center bottom'}) - */ - create(config) { - const tooltip = new Tooltip(this.psv, this.size); - tooltip.show(config); - - return tooltip; - } - -} diff --git a/src/styles/buttons/autorotate.scss b/src/styles/buttons/autorotate.scss deleted file mode 100644 index 85d59e343..000000000 --- a/src/styles/buttons/autorotate.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import '../vars'; - -.psv-autorotate-button { - &.psv-button { - width: #{$psv-buttons-height + $psv-buttons-padding * .5}; - height: #{$psv-buttons-height + $psv-buttons-padding * .5}; - padding: #{$psv-buttons-padding * .25 * 3}; - } -} diff --git a/src/styles/buttons/zoom-range.scss b/src/styles/buttons/zoom-range.scss deleted file mode 100644 index 5adbad3f1..000000000 --- a/src/styles/buttons/zoom-range.scss +++ /dev/null @@ -1,40 +0,0 @@ -@import '../vars'; - -.psv-zoom-range { - &.psv-button { - width: $psv-zoom-range-width; - height: $psv-zoom-range-tickness; - margin: $psv-buttons-padding 0; - padding: #{($psv-buttons-height - $psv-zoom-range-tickness) * .5} 0; - max-width: $psv-zoom-range-media-min-width; // trick for JS access - } - - &-line { - position: relative; - width: $psv-zoom-range-width; - height: $psv-zoom-range-tickness; - background: $psv-buttons-color; - transition: all .3s ease; - } - - &-handle { - position: absolute; - border-radius: 50%; - top: #{($psv-zoom-range-tickness - $psv-zoom-disk-diameter) * .5}; - width: $psv-zoom-disk-diameter; - height: $psv-zoom-disk-diameter; - background: $psv-buttons-color; - transform: scale(1); - transition: transform .3s ease; - } - - &:not(.psv-button--disabled):hover { - .psv-zoom-range-line { - box-shadow: 0 0 2px $psv-buttons-color; - } - - .psv-zoom-range-handle { - transform: scale(1.3); - } - } -} diff --git a/src/styles/loader.scss b/src/styles/loader.scss deleted file mode 100644 index a62c76700..000000000 --- a/src/styles/loader.scss +++ /dev/null @@ -1,49 +0,0 @@ -@import 'vars'; - -.psv-loader-container { - display: flex; - align-items: center; - justify-content: center; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: $psv-loader-zindex; -} - -// Pseudo element trick to vertically center elements -.psv-loader { - position: relative; - text-align: center; - color: $psv-loader-color; - width: $psv-loader-width; - height: $psv-loader-width; - border: $psv-loader-tickness solid transparent; - - &::before { - content: ''; - display: inline-block; - height: 100%; - vertical-align: middle; - } - - &, - &-image, - &-text { - display: inline-block; - vertical-align: middle; - } - - &-canvas { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - - &-text { - font: $psv-loader-font; - } -} diff --git a/src/styles/navbar.scss b/src/styles/navbar.scss deleted file mode 100644 index 64cbfcfc6..000000000 --- a/src/styles/navbar.scss +++ /dev/null @@ -1,90 +0,0 @@ -@use 'sass:list'; -@import 'vars'; - -.psv-navbar { - display: flex; - position: absolute; - z-index: $psv-navbar-zindex; - bottom: -$psv-navbar-height; - left: 0; - width: 100%; - height: $psv-navbar-height; - background: $psv-navbar-background; - transition: bottom ease-in-out .1s; - - &--open { - bottom: 0; - } - - &, - & * { - box-sizing: content-box; - } -} - -.psv-caption { - flex: 1 1 100%; - color: $psv-caption-color; - overflow: hidden; - text-align: center; - - &-icon { - height: $psv-buttons-height; - width: $psv-buttons-height; - cursor: pointer; - - * { - fill: $psv-buttons-color; - } - } - - &-content { - display: inline-block; - padding: $psv-buttons-padding; - font: $psv-caption-font; - white-space: nowrap; - } -} - -.psv-button { - flex: 0 0 auto; - padding: $psv-buttons-padding; - position: relative; - cursor: pointer; - height: $psv-buttons-height; - width: $psv-buttons-height; - background: $psv-buttons-background; - color: $psv-buttons-color; - - &--active { - background: $psv-buttons-active-background; - } - - &--disabled { - pointer-events: none; - opacity: $psv-buttons-disabled-opacity; - } - - &-svg { - width: 100%; - transform: scale(1); - transition: transform $psv-buttons-hover-scale-delay ease; - } -} - -.psv-button:not(.psv-button--disabled):focus-visible { - outline: $psv-element-focus-outline; - outline-offset: -#{list.nth($psv-element-focus-outline, 1)}; -} - -.psv-container:not(.psv--is-touch) .psv-button--hover-scale:not(.psv-button--disabled):hover .psv-button-svg { - transform: scale($psv-buttons-hover-scale); -} - -.psv-move-button + .psv-move-button { - margin-left: -$psv-buttons-padding; -} - -.psv-custom-button { - width: auto; -} diff --git a/src/styles/notification.scss b/src/styles/notification.scss deleted file mode 100644 index 83f9e7658..000000000 --- a/src/styles/notification.scss +++ /dev/null @@ -1,31 +0,0 @@ -@use 'sass:map'; -@import 'vars'; - -.psv-notification { - position: absolute; - z-index: $psv-notification-zindex; - bottom: $psv-notification-position-from; - display: flex; - justify-content: center; - box-sizing: border-box; - width: 100%; - padding: 0 2em; - opacity: 0; - transition-property: opacity, bottom; - transition-timing-function: ease-in-out; - transition-duration: $psv-notification-animate-delay; - - &-content { - max-width: 50em; - background-color: $psv-notification-background-color; - border-radius: $psv-notification-radius; - padding: $psv-notification-padding; - font: $psv-notification-font; - color: $psv-notification-text-color; - } - - &--visible { - opacity: 100; - bottom: $psv-notification-position-to; - } -} diff --git a/src/styles/overlay.scss b/src/styles/overlay.scss deleted file mode 100644 index 2eb82c9ae..000000000 --- a/src/styles/overlay.scss +++ /dev/null @@ -1,41 +0,0 @@ -@use 'sass:map'; -@import 'vars'; - -.psv-overlay { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - position: absolute; - z-index: $psv-overlay-zindex; - top: 0; - left: 0; - bottom: 0; - right: 0; - background: $psv-main-background; - color: $psv-overlay-color; - opacity: $psv-overlay-opacity; - - &-image { - margin-bottom: 4vh; - - svg { - width: map.get($psv-overlay-image-size, portrait); - - @media (orientation: landscape) { - width: map.get($psv-overlay-image-size, landscape); - } - } - } - - &-text { - font: $psv-overlay-text-size $psv-overlay-font-family; - text-align: center; - } - - &-subtext { - font: $psv-overlay-subtext-size $psv-overlay-font-family; - opacity: .8; - text-align: center; - } -} diff --git a/src/styles/panel.scss b/src/styles/panel.scss deleted file mode 100644 index 9ab03225f..000000000 --- a/src/styles/panel.scss +++ /dev/null @@ -1,242 +0,0 @@ -@use 'sass:list'; -@import 'vars'; - -@function make-dot-shadow($color, $w, $h) { - $val: 1px 0 $color; - $x: 3; - $y: 0; - - @while $y < $h { - @if $x > $w { - $x: 1; - $y: $y + 2; - } @else { - $val: #{$val}, #{$x}px #{$y}px #{$color}; - $x: $x + 2; - } - } - - @return $val; -} - -.psv-panel { - position: absolute; - z-index: $psv-panel-zindex; - right: 0; - height: 100%; - width: $psv-panel-width; - max-width: calc(100% - #{$psv-panel-close-button-width}); - background: $psv-panel-background; - transform: translate3d(100%, 0, 0); - opacity: 0; - transition-property: opacity, transform; - transition-timing-function: ease-in-out; - transition-duration: .1s; - cursor: default; - margin-left: $psv-panel-resizer-width; - - .psv--has-navbar & { - height: calc(100% - #{$psv-navbar-height}); - } - - &-close-button { - display: none; - position: absolute; - top: 0; - left: -$psv-panel-close-button-width; - width: $psv-panel-close-button-width; - height: $psv-panel-close-button-width; - background: $psv-panel-close-button-background; - - &::before, - &::after { - content: ''; - position: absolute; - top: 50%; - left: 4px; - width: $psv-panel-close-button-width - 9px; - height: 1px; - background-color: $psv-panel-close-button-color; - transition: .2s ease-in-out; - transition-property: width, left, transform; - } - - &::before { - transform: rotate(45deg); - } - - &::after { - transform: rotate(-45deg); - } - - &:hover { - &::before, - &::after { - left: 0; - width: $psv-panel-close-button-width - 1px; - } - - &::before { - transform: rotate(135deg); - } - - &::after { - transform: rotate(45deg); - } - } - } - - &-resizer { - display: none; - position: absolute; - top: 0; - left: -$psv-panel-resizer-width; - width: $psv-panel-resizer-width; - height: 100%; - background-color: $psv-panel-resizer-background; - cursor: col-resize; - - $psv-panel-resizer-grip-width: $psv-panel-resizer-width - 4px; - - @if $psv-panel-resizer-grip-width > 0 { - &::before { - content: ''; - position: absolute; - top: 50%; - left: ($psv-panel-resizer-width - $psv-panel-resizer-grip-width) * .5 - 1px; - margin-top: (-$psv-panel-resizer-grip-height * .5); - width: 1px; - height: 1px; - box-shadow: make-dot-shadow($psv-panel-resizer-grip-color, $psv-panel-resizer-grip-width, $psv-panel-resizer-grip-height); - background: transparent; - } - } - } - - &-content { - width: 100%; - height: 100%; - box-sizing: border-box; - color: $psv-panel-text-color; - font: $psv-panel-font; - overflow: auto; - - &:not(&--no-margin) { - padding: $psv-panel-padding; - } - - &--no-interaction { - user-select: none; - pointer-events: none; - } - } - - &--open { - transform: translate3d(0, 0, 0); - opacity: 1; - transition-duration: .2s; - - .psv-panel-close-button, - .psv-panel-resizer { - display: block; - } - } - - @media screen and (max-width: #{$psv-panel-width}) { - width: 100%; - max-width: none; - - &-resizer { - display: none; - } - - &-close-button { - left: 0; - } - } -} - -.psv-panel-menu { - height: 100%; - display: flex; - flex-direction: column; - - &-title { - flex: none; - display: flex; - align-items: center; - font: $psv-panel-title-font; - margin: $psv-panel-title-margin $psv-panel-title-margin * .5; - - svg { - width: $psv-panel-title-icon-size; - height: $psv-panel-title-icon-size; - margin-right: $psv-panel-title-margin * .5; - } - } - - &-list { - flex: 1; - list-style: none; - margin: 0; - padding: 0; - overflow-x: hidden; - } - - &-item { - min-height: $psv-panel-menu-item-height; - padding: $psv-panel-menu-item-padding; - cursor: pointer; - display: flex; - align-items: center; - justify-content: flex-start; - transition: background .1s ease-in-out; - - &--active { - outline: $psv-panel-menu-item-active-outline solid currentcolor; - outline-offset: -$psv-panel-menu-item-active-outline; - } - - &-icon { - flex: none; - height: $psv-panel-menu-item-height; - width: $psv-panel-menu-item-height; - margin-right: #{list.nth($psv-panel-menu-item-padding, 1)}; - - img { - max-width: 100%; - max-height: 100%; - } - - svg { - width: 100%; - height: 100%; - } - } - - &:focus-visible { - outline: $psv-element-focus-outline; - outline-offset: -#{list.nth($psv-element-focus-outline, 1)}; - } - } - - &--stripped &-item { - &:hover { - background: $psv-panel-menu-hover-background; - } - - &:nth-child(odd), - &:nth-child(odd)::before { - background: $psv-panel-menu-odd-background; - } - - &:nth-child(even), - &:nth-child(even)::before { - background: $psv-panel-menu-even-background; - } - } -} - -.psv-container:not(.psv--is-touch) .psv-panel-menu-item:hover { - background: $psv-panel-menu-hover-background; -} diff --git a/src/styles/tooltip.scss b/src/styles/tooltip.scss deleted file mode 100644 index b4ab7509e..000000000 --- a/src/styles/tooltip.scss +++ /dev/null @@ -1,110 +0,0 @@ -@import 'vars'; - -.psv-tooltip { - position: absolute; - z-index: $psv-tooltip-zindex; - box-sizing: border-box; - max-width: $psv-tooltip-max-width; - background-color: $psv-tooltip-background-color; - border-radius: $psv-tooltip-radius; - padding: $psv-tooltip-padding; - opacity: 0; - transition-property: opacity, transform; - transition-timing-function: ease-in-out; - transition-duration: $psv-tooltip-animate-delay; - - &-content { - color: $psv-tooltip-text-color; - font: $psv-tooltip-font; - text-shadow: $psv-tooltip-text-shadow; - } - - &-arrow { - position: absolute; - height: 0; - width: 0; - border: $psv-tooltip-arrow-size solid transparent; - } - - &--top-left, - &--top-center, - &--top-right { - transform: translate3d(0, $psv-tooltip-animate-offset, 0); - - .psv-tooltip-arrow { - border-top-color: $psv-tooltip-background-color; - } - } - - &--bottom-left, - &--bottom-center, - &--bottom-right { - transform: translate3d(0, -$psv-tooltip-animate-offset, 0); - - .psv-tooltip-arrow { - border-bottom-color: $psv-tooltip-background-color; - } - } - - &--left-top, - &--center-left, - &--left-bottom { - transform: translate3d($psv-tooltip-animate-offset, 0, 0); - - .psv-tooltip-arrow { - border-left-color: $psv-tooltip-background-color; - } - } - - &--right-top, - &--center-right, - &--right-bottom { - transform: translate3d(-$psv-tooltip-animate-offset, 0, 0); - - .psv-tooltip-arrow { - border-right-color: $psv-tooltip-background-color; - } - } - - &--left-top, - &--top-left { - box-shadow: #{-$psv-tooltip-shadow-offset} #{-$psv-tooltip-shadow-offset} 0 $psv-tooltip-shadow-color; - } - - &--top-center { - box-shadow: 0 #{-$psv-tooltip-shadow-offset} 0 $psv-tooltip-shadow-color; - } - - &--right-top, - &--top-right { - box-shadow: $psv-tooltip-shadow-offset #{-$psv-tooltip-shadow-offset} 0 $psv-tooltip-shadow-color; - } - - &--left-bottom, - &--bottom-left { - box-shadow: #{-$psv-tooltip-shadow-offset} $psv-tooltip-shadow-offset 0 $psv-tooltip-shadow-color; - } - - &--bottom-center { - box-shadow: 0 $psv-tooltip-shadow-offset 0 $psv-tooltip-shadow-color; - } - - &--right-bottom, - &--bottom-right { - box-shadow: $psv-tooltip-shadow-offset $psv-tooltip-shadow-offset 0 $psv-tooltip-shadow-color; - } - - &--center-left { - box-shadow: #{-$psv-tooltip-shadow-offset} 0 0 $psv-tooltip-shadow-color; - } - - &--center-right { - box-shadow: $psv-tooltip-shadow-offset 0 0 $psv-tooltip-shadow-color; - } - - &--visible { - transform: translate3d(0, 0, 0); - opacity: 1; - transition-duration: $psv-tooltip-animate-delay; - } -} diff --git a/src/styles/viewer.scss b/src/styles/viewer.scss deleted file mode 100644 index 2641d4e00..000000000 --- a/src/styles/viewer.scss +++ /dev/null @@ -1,31 +0,0 @@ -@import 'vars'; - -.psv-container { - width: 100%; - height: 100%; - margin: 0; - padding: 0; - position: relative; - background: $psv-main-background; - overflow: hidden; -} - -.psv-container--fullscreen { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; -} - -.psv-canvas-container { - position: absolute; - top: 0; - left: 0; - z-index: $psv-canvas-zindex; - transition: opacity linear 100ms; -} - -.psv-canvas { - display: block; -} diff --git a/src/utils/Animation.js b/src/utils/Animation.js deleted file mode 100644 index 9f1f5c82d..000000000 --- a/src/utils/Animation.js +++ /dev/null @@ -1,159 +0,0 @@ -import { EASINGS } from '../data/constants'; -import { each } from './misc'; - -/** - * @callback OnTick - * @summary Function called for each animation frame with computed properties - * @memberOf PSV.utils.Animation - * @param {Object.} properties - current values - * @param {float} progress - 0 to 1 - */ - -/** - * @summary Interpolation helper for animations - * @memberOf PSV.utils - * @description - * Implements the Promise API with an additional "cancel" method. - * The promise is resolved with `true` when the animation is completed and `false` if the animation is cancelled. - * @example - * const anim = new Animation({ - * properties: { - * width: {start: 100, end: 200} - * }, - * duration: 5000, - * onTick: (properties) => element.style.width = `${properties.width}px`; - * }); - * - * anim.then((completed) => ...); - * - * anim.cancel() - */ -export class Animation { - - /** - * @param {Object} options - * @param {Object.} options.properties - * @param {number} options.properties[].start - * @param {number} options.properties[].end - * @param {number} options.duration - * @param {number} [options.delay=0] - * @param {string} [options.easing='linear'] - * @param {PSV.utils.Animation.OnTick} options.onTick - called on each frame - */ - constructor(options) { - this.__callbacks = []; - - if (options) { - if (!options.easing || typeof options.easing === 'string') { - options.easing = EASINGS[options.easing || 'linear']; - } - - this.__start = null; - this.options = options; - - if (options.delay) { - this.__delayTimeout = setTimeout(() => { - this.__delayTimeout = null; - this.__animationFrame = window.requestAnimationFrame(t => this.__run(t)); - }, options.delay); - } - else { - this.__animationFrame = window.requestAnimationFrame(t => this.__run(t)); - } - } - else { - this.__resolved = true; - } - } - - /** - * @summary Main loop for the animation - * @param {number} timestamp - * @private - */ - __run(timestamp) { - if (this.__cancelled) { - return; - } - - // first iteration - if (this.__start === null) { - this.__start = timestamp; - } - - // compute progress - const progress = (timestamp - this.__start) / this.options.duration; - const current = {}; - - if (progress < 1.0) { - // interpolate properties - each(this.options.properties, (prop, name) => { - if (prop) { - current[name] = prop.start + (prop.end - prop.start) * this.options.easing(progress); - } - }); - this.options.onTick(current, progress); - - this.__animationFrame = window.requestAnimationFrame(t => this.__run(t)); - } - else { - // call onTick one last time with final values - each(this.options.properties, (prop, name) => { - if (prop) { - current[name] = prop.end; - } - }); - this.options.onTick(current, 1.0); - - this.__animationFrame = window.requestAnimationFrame(() => { - this.__resolved = true; - this.__resolve(true); - }); - } - } - - /** - * @private - */ - __resolve(value) { - this.__callbacks.forEach(cb => cb(value)); - this.__callbacks.length = 0; - } - - /** - * @summary Promise chaining - * @param {Function} [onFulfilled] - Called when the animation is complete (true) or cancelled (false) - * @returns {Promise} - */ - then(onFulfilled) { - if (this.__resolved || this.__cancelled) { - return Promise.resolve(this.__resolved) - .then(onFulfilled); - } - - return new Promise((resolve) => { - this.__callbacks.push(resolve); - }) - .then(onFulfilled); - } - - /** - * @summary Cancels the animation - */ - cancel() { - if (!this.__cancelled && !this.__resolved) { - this.__cancelled = true; - this.__resolve(false); - - if (this.__delayTimeout) { - window.clearTimeout(this.__delayTimeout); - this.__delayTimeout = null; - } - if (this.__animationFrame) { - window.cancelAnimationFrame(this.__animationFrame); - this.__animationFrame = null; - } - } - } - -} diff --git a/src/utils/Dynamic.js b/src/utils/Dynamic.js deleted file mode 100644 index 08d079938..000000000 --- a/src/utils/Dynamic.js +++ /dev/null @@ -1,212 +0,0 @@ -import { MathUtils } from 'three'; -import { PSVError } from '../PSVError'; -import { loop } from './math'; - -/** - * @summary Represents a variable that can dynamically change with time (using requestAnimationFrame) - * @memberOf PSV.utils - */ -export class Dynamic { - - static STOP = 0; - static INFINITE = 1; - static POSITION = 2; - - /** - * @param {Function} [fn] Callback function - * @param {number} [defaultValue] Default position - * @param {number} [min] Minimum position - * @param {number} [max] Maximum position - * @param {boolean} [loopValue] Loop value between min and max - */ - constructor(fn, defaultValue = 0, min = -Infinity, max = Infinity, loopValue = false) { - /** - * @type {Function} - * @private - * @readonly - */ - this.fn = fn; - - /** - * @type {number} - * @private - */ - this.mode = Dynamic.STOP; - - /** - * @type {number} - * @private - */ - this.speed = 0; - - /** - * @type {number} - * @private - */ - this.speedMult = 1; - - /** - * @type {number} - * @private - */ - this.currentSpeed = 0; - - /** - * @type {number} - * @private - */ - this.target = 0; - - /** - * @type {number} - * @readonly - */ - this.current = defaultValue; - - /** - * @type {number} - * @private - */ - this.min = min; - - /** - * @type {number} - * @private - */ - this.max = max; - - /** - * @type {boolean} - * @private - */ - this.loopValue = loopValue; - - if (loopValue && min !== 0) { - throw new PSVError('invalid config'); - } - - if (this.fn) { - this.fn(defaultValue); - } - } - - /** - * Changes base speed - * @param {number} speed - */ - setSpeed(speed) { - this.speed = speed; - } - - /** - * Defines the target position - * @param {number} position - * @param {number} [speedMult=1] - */ - goto(position, speedMult = 1) { - this.mode = Dynamic.POSITION; - this.target = this.loopValue ? loop(position, this.max) : MathUtils.clamp(position, this.min, this.max); - this.speedMult = speedMult; - } - - /** - * Increase/decrease the target position - * @param {number} step - * @param {number} [speedMult=1] - */ - step(step, speedMult = 1) { - if (this.mode !== Dynamic.POSITION) { - this.target = this.current; - } - this.goto(this.target + step, speedMult); - } - - /** - * Starts infinite movement - * @param {boolean} [invert=false] - * @param {number} [speedMult=1] - */ - roll(invert = false, speedMult = 1) { - this.mode = Dynamic.INFINITE; - this.target = invert ? -Infinity : Infinity; - this.speedMult = speedMult; - } - - /** - * Stops movement - */ - stop() { - this.mode = Dynamic.STOP; - } - - /** - * Defines the current position and immediately stops movement - * @param {number} value - */ - setValue(value) { - this.target = this.loopValue ? loop(value, this.max) : MathUtils.clamp(value, this.min, this.max); - this.mode = Dynamic.STOP; - if (this.target !== this.current) { - this.current = this.target; - if (this.fn) { - this.fn(this.current); - } - return true; - } - return false; - } - - /** - * @package - */ - update(elapsed) { - // in position mode switch to stop mode when in the decceleration window - if (this.mode === Dynamic.POSITION) { - // in loop mode, alter "current" to avoid crossing the origin - if (this.loopValue && Math.abs(this.target - this.current) > this.max / 2) { - this.current = this.current < this.target ? this.current + this.max : this.current - this.max; - } - - const dstStop = this.currentSpeed * this.currentSpeed / (this.speed * this.speedMult * 4); - if (Math.abs(this.target - this.current) <= dstStop) { - this.mode = Dynamic.STOP; - } - } - - // compute speed - let targetSpeed = this.mode === Dynamic.STOP ? 0 : this.speed * this.speedMult; - if (this.target < this.current) { - targetSpeed = -targetSpeed; - } - if (this.currentSpeed < targetSpeed) { - this.currentSpeed = Math.min(targetSpeed, this.currentSpeed + elapsed / 1000 * this.speed * this.speedMult * 2); - } - else if (this.currentSpeed > targetSpeed) { - this.currentSpeed = Math.max(targetSpeed, this.currentSpeed - elapsed / 1000 * this.speed * this.speedMult * 2); - } - - // compute new position - let next = null; - if (this.current > this.target && this.currentSpeed) { - next = Math.max(this.target, this.current + this.currentSpeed * elapsed / 1000); - } - else if (this.current < this.target && this.currentSpeed) { - next = Math.min(this.target, this.current + this.currentSpeed * elapsed / 1000); - } - - // apply value - if (next !== null) { - next = this.loopValue ? loop(next, this.max) : MathUtils.clamp(next, this.min, this.max); - if (next !== this.current) { - this.current = next; - if (this.fn) { - this.fn(this.current); - } - return true; - } - } - - return false; - } - -} diff --git a/src/utils/DynamicXD.js b/src/utils/DynamicXD.js deleted file mode 100644 index 192bfa57d..000000000 --- a/src/utils/DynamicXD.js +++ /dev/null @@ -1,191 +0,0 @@ -import { MathUtils } from 'three'; -import { each } from './misc'; - -/** - * @summary Implementation of {@link PSV.utils.Dynamic} for any number of variables, unused - * @memberOf PSV.utils - * @private - */ -export class DynamicXD { - - static STOP = 0; - static INFINITE = 1; - static POSITION = 2; - - get current() { - return this.reduce((values, _) => { - values[_.name] = _.current; - return values; - }, {}); - } - - constructor(fn, _) { - this.fn = fn; - this.mode = DynamicXD.STOP; - this.speed = 0; - this.speedMult = 1; - this.currentSpeed = 0; - this._ = {}; - each(_, (dim, name) => { - this._[name] = { - min : -Infinity, - max : Infinity, - ...dim, - name : name, - target : 0, - current: 0, - }; - }); - } - - forEach(fn) { - each(this._, fn); - } - - reduce(fn, init) { - return Object.keys(this._).reduce((acc, name) => fn(acc, this._[name]), init); - } - - /** - * Defines the target position - */ - goto(positions, speedMult = 1) { - this.mode = DynamicXD.POSITION; - this.speedMult = speedMult; - this.forEach((_) => { - if (_.name in positions) { - _.target = MathUtils.clamp(positions[_.name], _.min, _.max); - } - }); - } - - /** - * Increase/decrease the target position - */ - step(steps, speedMult = 1) { - if (this.mode !== DynamicXD.POSITION) { - this.forEach((_) => { - _.target = _.current; - }); - } - this.mode = DynamicXD.POSITION; - this.speedMult = speedMult; - this.forEach((_) => { - if (_.name in steps) { - _.target = MathUtils.clamp(_.target + steps[_.name], _.min, _.max); - } - }); - } - - /** - * Starts infinite movement - */ - roll(rolls, speedMult = 1) { - this.mode = DynamicXD.INFINITE; - this.speedMult = speedMult; - this.forEach((_) => { - if (_.name in rolls) { - _.target = rolls[_.name] ? -Infinity : Infinity; - } - else { - _.target = _.current; - } - }); - } - - /** - * Stops movement - */ - stop() { - this.mode = DynamicXD.STOP; - } - - /** - * Defines the current position and immediately stops movement - */ - setValue(values) { - this.mode = DynamicXD.STOP; - - const hasChanges = this.reduce((changes, _) => { - let changed = false; - if (_.name in values) { - const next = MathUtils.clamp(values[_.name], _.min, _.max); - changed = next !== _.current; - _.current = next; - } - _.target = _.current; - return changes || changed; - }, false); - - if (hasChanges && this.fn) { - this.fn(this.current); - } - - return hasChanges; - } - - /** - * @package - */ - update(elapsed) { - const elapsedS = elapsed / 1000; - const acceleration = this.speed * this.speedMult * 2; - - // in position mode switch to stop mode when in the decceleration window - if (this.mode === DynamicXD.POSITION) { - const dstStop = this.currentSpeed * this.currentSpeed / (acceleration * 2); - const dstCurr = this.reduce((dst, _) => { - return dst + (_.target - _.current) * (_.target - _.current); - }, 0); - - if (dstCurr <= dstStop * dstStop) { // no Math.sqrt on dstCurr - this.mode = DynamicXD.STOP; - } - } - - // FIXME the speed should be different for each component (with sum = global speed) - // FIXME implement signed speed for smooth changes of direction - - // compute speed - const targetSpeed = this.mode === DynamicXD.STOP ? 0 : this.speed * this.speedMult; - if (this.currentSpeed < targetSpeed) { - this.currentSpeed = Math.min(targetSpeed, this.currentSpeed + elapsedS * acceleration); - } - else if (this.currentSpeed > targetSpeed) { - this.currentSpeed = Math.max(targetSpeed, this.currentSpeed - elapsedS * acceleration); - } - - if (this.currentSpeed) { - // compute position - const hasChanges = this.reduce((changes, _) => { - let next = null; - if (_.current > _.target) { - next = Math.max(_.target, _.current - this.currentSpeed * elapsedS); - } - else if (_.current < _.target) { - next = Math.min(_.target, _.current + this.currentSpeed * elapsedS); - } - - if (next !== null) { - next = MathUtils.clamp(next, _.min, _.max); - if (next !== _.current) { - _.current = next; - return true; - } - } - - return changes; - }, false); - - // apply - if (hasChanges && this.fn) { - this.fn(this.current); - } - - return hasChanges; - } - - return false; - } - -} diff --git a/src/utils/MultiDynamic.js b/src/utils/MultiDynamic.js deleted file mode 100644 index 3e6efce7c..000000000 --- a/src/utils/MultiDynamic.js +++ /dev/null @@ -1,128 +0,0 @@ -import { each } from './misc'; - -/** - * @summary Wrapper for multiple {@link PSV.utils.Dynamic} evolving together - * @memberOf PSV.utils - */ -export class MultiDynamic { - - /** - * @type {Object} - * @readonly - */ - get current() { - const values = {}; - each(this.dynamics, (dynamic, name) => { - values[name] = dynamic.current; - }); - return values; - } - - /** - * @param {Record} dynamics - * @param {Function} [fn] Callback function - */ - constructor(dynamics, fn) { - /** - * @type {Function} - * @private - * @readonly - */ - this.fn = fn; - - /** - * @type {Record} - * @private - * @readonly - */ - this.dynamics = dynamics; - - if (this.fn) { - this.fn(this.current); - } - } - - /** - * Changes base speed - * @param {number} speed - */ - setSpeed(speed) { - each(this.dynamics, (d) => { - d.setSpeed(speed); - }); - } - - /** - * Defines the target positions - * @param {Record} positions - * @param {number} [speedMult=1] - */ - goto(positions, speedMult = 1) { - each(positions, (position, name) => { - this.dynamics[name].goto(position, speedMult); - }); - } - - /** - * Increase/decrease the target positions - * @param {Record} steps - * @param {number} [speedMult=1] - */ - step(steps, speedMult = 1) { - each(steps, (step, name) => { - this.dynamics[name].step(step, speedMult); - }); - } - - /** - * Starts infinite movements - * @param {Record} rolls - * @param {number} [speedMult=1] - */ - roll(rolls, speedMult = 1) { - each(rolls, (roll, name) => { - this.dynamics[name].roll(roll, speedMult); - }); - } - - /** - * Stops movements - */ - stop() { - each(this.dynamics, d => d.stop()); - } - - /** - * Defines the current positions and immediately stops movements - * @param {Record} values - */ - setValue(values) { - let hasUpdates = false; - each(values, (value, name) => { - hasUpdates |= this.dynamics[name].setValue(value); - }); - - if (hasUpdates && this.fn) { - this.fn(this.current); - } - - return hasUpdates; - } - - /** - * @package - */ - update(elapsed) { - let hasUpdates = false; - each(this.dynamics, (dynamic) => { - hasUpdates |= dynamic.update(elapsed); - }); - - if (hasUpdates && this.fn) { - this.fn(this.current); - } - - return hasUpdates; - } - -} diff --git a/src/utils/PressHandler.js b/src/utils/PressHandler.js deleted file mode 100644 index 387edf907..000000000 --- a/src/utils/PressHandler.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @summary Helper for pressable things (buttons, keyboard) - * @description When the pressed thing goes up and was not pressed long enough, wait a bit more before execution - * @private - */ -export class PressHandler { - - constructor(delay = 200) { - this.delay = delay; - this.time = 0; - this.timeout = null; - } - - down() { - if (this.timeout) { - clearTimeout(this.timeout); - this.timeout = null; - } - - this.time = new Date().getTime(); - } - - up(cb) { - if (!this.time) { - return; - } - - const elapsed = new Date().getTime() - this.time; - if (elapsed < this.delay) { - this.timeout = setTimeout(() => { - cb(); - this.timeout = null; - this.time = 0; - }, this.delay); - } - else { - cb(); - this.time = 0; - } - } - -} diff --git a/src/utils/PressHandler.spec.js b/src/utils/PressHandler.spec.js deleted file mode 100644 index 5c6c649c9..000000000 --- a/src/utils/PressHandler.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import assert from 'assert'; -import { PressHandler } from './PressHandler'; - -describe('utils:PressHandler', () => { - it('should wait at least X ms before exec', (done) => { - const handler = new PressHandler(100); - - const start = new Date().getTime(); - - handler.down(); - handler.up(() => { - const elapsed = new Date().getTime() - start; - assert.ok(elapsed >= 100); - done(); - }); - }); - - it('should exec immediately if X ms already elapsed', (done) => { - const handler = new PressHandler(100); - - handler.down(); - - setTimeout(() => { - const start = new Date().getTime(); - handler.up(() => { - const elapsed = new Date().getTime() - start; - assert.ok(elapsed < 5); - done(); - }); - }, 200); - }); -}); diff --git a/src/utils/Slider.js b/src/utils/Slider.js deleted file mode 100644 index 9296a03ef..000000000 --- a/src/utils/Slider.js +++ /dev/null @@ -1,187 +0,0 @@ -import { EventEmitter } from 'uevent'; - -/** - * @summary Helper to make sliders elements - * @memberOf PSV.utils - */ -export class Slider extends EventEmitter { - - static VERTICAL = 1; - static HORIZONTAL = 2; - - /** - * @type {boolean} - * @readonly - */ - get vertical() { - return this.prop.direction === Slider.VERTICAL; - } - - constructor({ psv, container, direction, onUpdate }) { - super(); - - /** - * @summary Reference to main controller - * @type {PSV.Viewer} - * @readonly - */ - this.psv = psv; - - /** - * @member {HTMLElement} - * @readonly - */ - this.container = container; - - /** - * @summary Internal properties - * @member {Object} - * @protected - * @property {boolean} mousedown - * @property {number} mediaMinWidth - */ - this.prop = { - onUpdate : onUpdate, - direction: direction, - mousedown: false, - mouseover: false, - }; - - this.container.addEventListener('click', this); - this.container.addEventListener('mousedown', this); - this.container.addEventListener('mouseenter', this); - this.container.addEventListener('mouseleave', this); - this.container.addEventListener('touchstart', this); - this.container.addEventListener('mousemove', this, true); - this.container.addEventListener('touchmove', this, true); - window.addEventListener('mouseup', this); - window.addEventListener('touchend', this); - } - - /** - * @protected - */ - destroy() { - window.removeEventListener('mouseup', this); - window.removeEventListener('touchend', this); - } - - /** - * @summary Handles events - * @param {Event} e - * @private - */ - handleEvent(e) { - /* eslint-disable */ - switch (e.type) { - // @formatter:off - case 'click': e.stopPropagation(); break; - case 'mousedown': this.__onMouseDown(e); break; - case 'mouseenter': this.__onMouseEnter(e); break; - case 'mouseleave': this.__onMouseLeave(e); break; - case 'touchstart': this.__onTouchStart(e); break; - case 'mousemove': this.__onMouseMove(e); break; - case 'touchmove': this.__onTouchMove(e); break; - case 'mouseup': this.__onMouseUp(e); break; - case 'touchend': this.__onTouchEnd(e); break; - // @formatter:on - } - /* eslint-enable */ - } - - /** - * @private - */ - __onMouseDown(evt) { - this.prop.mousedown = true; - this.__update(evt, true); - } - - /** - * @private - */ - __onMouseEnter(evt) { - this.prop.mouseover = true; - this.__update(evt, true); - } - - /** - * @private - */ - __onTouchStart(evt) { - this.prop.mouseover = true; - this.prop.mousedown = true; - this.__update(evt.changedTouches[0], true); - } - - /** - * @private - */ - __onMouseMove(evt) { - if (this.prop.mousedown || this.prop.mouseover) { - evt.stopPropagation(); - this.__update(evt, true); - } - } - - /** - * @private - */ - __onTouchMove(evt) { - if (this.prop.mousedown || this.prop.mouseover) { - evt.stopPropagation(); - this.__update(evt.changedTouches[0], true); - } - } - - /** - * @private - */ - __onMouseUp(evt) { - if (this.prop.mousedown) { - this.prop.mousedown = false; - this.__update(evt, false); - } - } - - /** - * @private - */ - __onMouseLeave(evt) { - if (this.prop.mouseover) { - this.prop.mouseover = false; - this.__update(evt, true); - } - } - - /** - * @private - */ - __onTouchEnd(evt) { - if (this.prop.mousedown) { - this.prop.mouseover = false; - this.prop.mousedown = false; - this.__update(evt.changedTouches[0], false); - } - } - - /** - * @private - */ - __update(evt, moving) { - const boundingClientRect = this.container.getBoundingClientRect(); - const cursor = evt[this.vertical ? 'clientY' : 'clientX']; - const pos = boundingClientRect[this.vertical ? 'bottom' : 'left']; - const size = boundingClientRect[this.vertical ? 'height' : 'width']; - const val = Math.abs((pos - cursor) / size); - - this.prop.onUpdate({ - value : val, - click : !moving, - mousedown: this.prop.mousedown, - mouseover: this.prop.mouseover, - cursor : evt, - }); - } - -} diff --git a/src/utils/browser.js b/src/utils/browser.js deleted file mode 100644 index 2e47144fa..000000000 --- a/src/utils/browser.js +++ /dev/null @@ -1,215 +0,0 @@ -/** - * @summary Toggles a CSS class - * @memberOf PSV.utils - * @param {HTMLElement|SVGElement} element - * @param {string} className - * @param {boolean} [active] - forced state - */ -export function toggleClass(element, className, active) { - if (active === undefined) { - element.classList.toggle(className); - } - else if (active) { - element.classList.add(className); - } - else if (!active) { - element.classList.remove(className); - } -} - -/** - * @summary Adds one or several CSS classes to an element - * @memberOf PSV.utils - * @param {HTMLElement} element - * @param {string} className - */ -export function addClasses(element, className) { - element.classList.add(...className.split(' ')); -} - -/** - * @summary Removes one or several CSS classes to an element - * @memberOf PSV.utils - * @param {HTMLElement} element - * @param {string} className - */ -export function removeClasses(element, className) { - element.classList.remove(...className.split(' ')); -} - -/** - * @summary Searches if an element has a particular parent at any level including itself - * @memberOf PSV.utils - * @param {HTMLElement} el - * @param {HTMLElement} parent - * @returns {boolean} - */ -export function hasParent(el, parent) { - let test = el; - - do { - if (test === parent) { - return true; - } - test = test.parentNode; - } while (test); - - return false; -} - -/** - * @summary Gets the closest parent (can by itself) - * @memberOf PSV.utils - * @param {HTMLElement|SVGElement} el - * @param {string} selector - * @returns {HTMLElement} - */ -export function getClosest(el, selector) { - // When el is document or window, the matches does not exist - if (!el?.matches) { - return null; - } - - let test = el; - - do { - if (test.matches(selector)) { - return test; - } - test = test instanceof SVGElement ? test.parentNode : test.parentElement; - } while (test); - - return null; -} - -/** - * @summary Gets the position of an element in the viewer without reflow - * @description Will gives the same result as getBoundingClientRect() as soon as there are no CSS transforms - * @memberOf PSV.utils - * @param {HTMLElement} el - * @return {{left: number, top: number}} - */ -export function getPosition(el) { - let left = 0; - let top = 0; - let test = el; - - while (test) { - left += (test.offsetLeft - test.scrollLeft + test.clientLeft); - top += (test.offsetTop - test.scrollTop + test.clientTop); - test = test.offsetParent; - } - - return { left, top }; -} - -/** - * @summary Detects if fullscreen is enabled - * @memberOf PSV.utils - * @param {HTMLElement} elt - * @returns {boolean} - */ -export function isFullscreenEnabled(elt) { - return (document.fullscreenElement || document.webkitFullscreenElement) === elt; -} - -/** - * @summary Enters fullscreen mode - * @memberOf PSV.utils - * @param {HTMLElement} elt - */ -export function requestFullscreen(elt) { - (elt.requestFullscreen || elt.webkitRequestFullscreen).call(elt); -} - -/** - * @summary Exits fullscreen mode - * @memberOf PSV.utils - */ -export function exitFullscreen() { - (document.exitFullscreen || document.webkitExitFullscreen).call(document); -} - -/** - * @summary Gets an element style - * @memberOf PSV.utils - * @param {HTMLElement} elt - * @param {string} prop - * @returns {*} - */ -export function getStyle(elt, prop) { - return window.getComputedStyle(elt, null)[prop]; -} - -/** - * @summary Normalize mousewheel values accross browsers - * @memberOf PSV.utils - * @description From Facebook's Fixed Data Table - * {@link https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js} - * @copyright Facebook - * @param {WheelEvent} event - * @returns {{spinX: number, spinY: number, pixelX: number, pixelY: number}} - */ -export function normalizeWheel(event) { - const PIXEL_STEP = 10; - const LINE_HEIGHT = 40; - const PAGE_HEIGHT = 800; - - let spinX = 0; - let spinY = 0; - let pixelX = 0; - let pixelY = 0; - - // Legacy - if ('detail' in event) { - spinY = event.detail; - } - if ('wheelDelta' in event) { - spinY = -event.wheelDelta / 120; - } - if ('wheelDeltaY' in event) { - spinY = -event.wheelDeltaY / 120; - } - if ('wheelDeltaX' in event) { - spinX = -event.wheelDeltaX / 120; - } - - // side scrolling on FF with DOMMouseScroll - if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) { - spinX = spinY; - spinY = 0; - } - - pixelX = spinX * PIXEL_STEP; - pixelY = spinY * PIXEL_STEP; - - if ('deltaY' in event) { - pixelY = event.deltaY; - } - if ('deltaX' in event) { - pixelX = event.deltaX; - } - - if ((pixelX || pixelY) && event.deltaMode) { - // delta in LINE units - if (event.deltaMode === 1) { - pixelX *= LINE_HEIGHT; - pixelY *= LINE_HEIGHT; - } - // delta in PAGE units - else { - pixelX *= PAGE_HEIGHT; - pixelY *= PAGE_HEIGHT; - } - } - - // Fall-back if spin cannot be determined - if (pixelX && !spinX) { - spinX = (pixelX < 1) ? -1 : 1; - } - if (pixelY && !spinY) { - spinY = (pixelY < 1) ? -1 : 1; - } - - return { spinX, spinY, pixelX, pixelY }; -} diff --git a/src/utils/math.js b/src/utils/math.js deleted file mode 100644 index d66587c83..000000000 --- a/src/utils/math.js +++ /dev/null @@ -1,106 +0,0 @@ -import { MathUtils } from 'three'; - -/** - * @deprecated use THREE.MathUtils.clamp - */ -export function bound(x, min, max) { - return MathUtils.clamp(x, min, max); -} - -/** - * @summary Ensure a value is within 0 and `max` - * @param {number} value - * @param {number} max - * @return {number} - */ -export function loop(value, max) { - let result = value % max; - - if (result < 0) { - result += max; - } - - return result; -} - -/** - * @deprecated Use THREE.MathUtils.isPowerOfTwo - */ -export function isPowerOfTwo(x) { - return MathUtils.isPowerOfTwo(x); -} - -/** - * @summary Computes the sum of an array - * @memberOf PSV.utils - * @param {number[]} array - * @returns {number} - */ -export function sum(array) { - return array.reduce((a, b) => a + b, 0); -} - -/** - * @summary Computes the distance between two points - * @memberOf PSV.utils - * @param {PSV.Point} p1 - * @param {PSV.Point} p2 - * @returns {number} - */ -export function distance(p1, p2) { - return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); -} - -/** - * @summary Compute the shortest offset between two longitudes - * @memberOf PSV.utils - * @param {number} from - * @param {number} to - * @returns {number} - */ -export function getShortestArc(from, to) { - const tCandidates = [ - 0, // direct - Math.PI * 2, // clock-wise cross zero - -Math.PI * 2, // counter-clock-wise cross zero - ]; - - return tCandidates.reduce((value, candidate) => { - const newCandidate = to - from + candidate; - return Math.abs(newCandidate) < Math.abs(value) ? newCandidate : value; - }, Infinity); -} - -/** - * @summary Computes the angle between the current position and a target position - * @memberOf PSV.utils - * @param {PSV.Position} position1 - * @param {PSV.Position} position2 - * @returns {number} - */ -export function getAngle(position1, position2) { - return Math.acos( - Math.cos(position1.latitude) - * Math.cos(position2.latitude) - * Math.cos(position1.longitude - position2.longitude) - + Math.sin(position1.latitude) - * Math.sin(position2.latitude) - ); -} - -/** - * @summary Returns the distance between two points on a sphere of radius one - * {@link http://www.movable-type.co.uk/scripts/latlong.html} - * @memberOf PSV.utils - * @param {number[]} p1 - * @param {number[]} p2 - * @returns {number} - */ -export function greatArcDistance(p1, p2) { - const [λ1, φ1] = p1; - const [λ2, φ2] = p2; - - const x = (λ2 - λ1) * Math.cos((φ1 + φ2) / 2); - const y = (φ2 - φ1); - return Math.sqrt(x * x + y * y); -} diff --git a/src/utils/misc.js b/src/utils/misc.js deleted file mode 100644 index 640d8ea29..000000000 --- a/src/utils/misc.js +++ /dev/null @@ -1,231 +0,0 @@ -/** - * @summary Transforms a string to dash-case {@link https://github.com/shahata/dasherize} - * @memberOf PSV.utils - * @param {string} str - * @returns {string} - */ -export function dasherize(str) { - return str.replace(/[A-Z](?:(?=[^A-Z])|[A-Z]*(?=[A-Z][^A-Z]|$))/g, (s, i) => { - return (i > 0 ? '-' : '') + s.toLowerCase(); - }); -} - -/** - * @summary Returns a function, that, when invoked, will only be triggered at most once during a given window of time. - * @memberOf PSV.utils - * @copyright underscore.js - modified by Clément Prévost {@link http://stackoverflow.com/a/27078401} - * @param {Function} func - * @param {number} wait - * @returns {Function} - */ -export function throttle(func, wait) { - /* eslint-disable */ - let self, args, result; - let timeout; - let previous = 0; - const later = function() { - previous = Date.now(); - timeout = undefined; - result = func.apply(self, args); - if (!timeout) { - self = args = null; - } - }; - return function() { - const now = Date.now(); - if (!previous) { - previous = now; - } - const remaining = wait - (now - previous); - self = this; - args = arguments; - if (remaining <= 0 || remaining > wait) { - if (timeout) { - clearTimeout(timeout); - timeout = undefined; - } - previous = now; - result = func.apply(self, args); - if (!timeout) { - self = args = null; - } - } - else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - /* eslint-enable */ -} - -/** - * @summary Test if an object is a plain object - * @memberOf PSV.utils - * @description Test if an object is a plain object, i.e. is constructed - * by the built-in Object constructor and inherits directly from Object.prototype - * or null. Some built-in objects pass the test, e.g. Math which is a plain object - * and some host or exotic objects may pass also. - * {@link http://stackoverflow.com/a/5878101/1207670} - * @param {*} obj - * @returns {boolean} - */ -export function isPlainObject(obj) { - // Basic check for Type object that's not null - if (typeof obj === 'object' && obj !== null) { - // If Object.getPrototypeOf supported, use it - if (typeof Object.getPrototypeOf === 'function') { - const proto = Object.getPrototypeOf(obj); - return proto === Object.prototype || proto === null; - } - - // Otherwise, use internal class - // This should be reliable as if getPrototypeOf not supported, is pre-ES5 - return Object.prototype.toString.call(obj) === '[object Object]'; - } - - // Not an object - return false; -} - -/** - * @summary Merges the enumerable attributes of two objects - * @memberOf PSV.utils - * @description Replaces arrays and alters the target object. - * @copyright Nicholas Fisher - * @param {Object} target - * @param {Object} src - * @returns {Object} target - */ -export function deepmerge(target, src) { - /* eslint-disable */ - let first = src; - - return (function merge(target, src) { - if (Array.isArray(src)) { - if (!target || !Array.isArray(target)) { - target = []; - } - else { - target.length = 0; - } - src.forEach(function(e, i) { - target[i] = merge(null, e); - }); - } - else if (typeof src === 'object') { - if (!target || Array.isArray(target)) { - target = {}; - } - Object.keys(src).forEach(function(key) { - if (typeof src[key] !== 'object' || !src[key] || !isPlainObject(src[key])) { - target[key] = src[key]; - } - else if (src[key] != first) { - if (!target[key]) { - target[key] = merge(null, src[key]); - } - else { - merge(target[key], src[key]); - } - } - }); - } - else { - target = src; - } - - return target; - }(target, src)); - /* eslint-enable */ -} - -/** - * @summary Deeply clones an object - * @memberOf PSV.utils - * @param {Object} src - * @returns {Object} - */ -export function clone(src) { - return deepmerge(null, src); -} - -/** - * @summery Test of an object is empty - * @memberOf PSV.utils - * @param {object} obj - * @returns {boolean} - */ -export function isEmpty(obj) { - return !obj || (Object.keys(obj).length === 0 && obj.constructor === Object); -} - -/** - * @summary Loops over enumerable properties of an object - * @memberOf PSV.utils - * @param {Object} object - * @param {Function} callback - */ -export function each(object, callback) { - Object.keys(object).forEach((key) => { - callback(object[key], key); - }); -} - -/** - * @summary Returns if a valu is null or undefined - * @memberOf PSV.utils - * @param {*} val - * @return {boolean} - */ -export function isNil(val) { - return val === null || val === undefined; -} - -/** - * @summary Returns the first non null non undefined parameter - * @memberOf PSV.utils - * @param {*} values - * @return {*} - */ -export function firstNonNull(...values) { - for (const val of values) { - if (!isNil(val)) { - return val; - } - } - - return undefined; -} - -/** - * @summary Returns deep equality between objects - * {@link https://gist.github.com/egardner/efd34f270cc33db67c0246e837689cb9} - * @param obj1 - * @param obj2 - * @return {boolean} - * @private - */ -export function deepEqual(obj1, obj2) { - if (obj1 === obj2) { - return true; - } - else if (isObject(obj1) && isObject(obj2)) { - if (Object.keys(obj1).length !== Object.keys(obj2).length) { - return false; - } - for (const prop of Object.keys(obj1)) { - if (!deepEqual(obj1[prop], obj2[prop])) { - return false; - } - } - return true; - } - else { - return false; - } -} - -function isObject(obj) { - return typeof obj === 'object' && obj != null; -} - diff --git a/src/utils/misc.spec.js b/src/utils/misc.spec.js deleted file mode 100644 index 2f2ce1402..000000000 --- a/src/utils/misc.spec.js +++ /dev/null @@ -1,137 +0,0 @@ -import assert from 'assert'; - -import { dasherize, deepEqual, deepmerge } from './misc'; - -describe('utils:misc:deepmerge', () => { - it('should merge basic plain objects', () => { - const one = { a: 'z', b: { c: { d: 'e' } } }; - const two = { b: { c: { f: 'g', j: 'i' } } }; - - const result = deepmerge(one, two); - - assert.deepStrictEqual(one, { a: 'z', b: { c: { d: 'e', f: 'g', j: 'i' } } }); - assert.strictEqual(result, one); - }); - - it('should merge arrays by replace', () => { - const one = { a: [1, 2, 3] }; - const two = { a: [2, 4] }; - - const result = deepmerge(one, two); - - assert.deepStrictEqual(one, { a: [2, 4] }); - assert.strictEqual(result, one); - }); - - it('should clone object', () => { - const one = { b: { c: { d: 'e' } } }; - - const result = deepmerge(null, one); - - assert.deepStrictEqual(result, { b: { c: { d: 'e' } } }); - assert.notStrictEqual(result, one); - assert.notStrictEqual(result.b.c, one.b.c); - }); - - it('should clone array', () => { - const one = [{ a: 'b' }, { c: 'd' }]; - - const result = deepmerge(null, one); - - assert.deepStrictEqual(result, [{ a: 'b' }, { c: 'd' }]); - assert.notStrictEqual(result[0], one[1]); - }); - - it('should accept primitives', () => { - const one = 'foo'; - const two = 'bar'; - - const result = deepmerge(one, two); - - assert.strictEqual(result, 'bar'); - }); - - it('should stop on recursion', () => { - const one = { a: 'foo' }; - one.b = one; - - const result = deepmerge(null, one); - - assert.deepStrictEqual(result, { a: 'foo' }); - }); -}); - -describe('utils:misc:dasherize', () => { - it('should dasherize from camelCase', () => { - assert.strictEqual(dasherize('strokeWidth'), 'stroke-width'); - }); - - it('should not change existing dash-case', () => { - assert.strictEqual(dasherize('stroke-width'), 'stroke-width'); - }); -}); - -describe('utils:misc:deepEqual', () => { - it('should compare simple objects', () => { - assert.strictEqual(deepEqual( - { foo: 'bar' }, - { foo: 'bar' }, - ), true); - - assert.strictEqual(deepEqual( - { foo: 'bar' }, - { foo: 'foo' }, - ), false); - - assert.strictEqual(deepEqual( - { foo: 'bar' }, - { foo: 'bar', baz: 'bar' }, - ), false); - }); - - it('should compare nested objects', () => { - assert.strictEqual(deepEqual( - { foo: { bar: 'baz' } }, - { foo: { bar: 'baz' } }, - ), true); - - assert.strictEqual(deepEqual( - { foo: { bar: 'baz' } }, - { foo: { bar: 'foo' } }, - ), false); - - assert.strictEqual(deepEqual( - { foo: { bar: 'baz' } }, - { foo: { bar: 'baz', baz: 'bar' } }, - ), false); - }); - - it('should compare arrays', () => { - assert.strictEqual(deepEqual( - { foo: ['bar', 'baz'] }, - { foo: ['bar', 'baz'] }, - ), true); - - assert.strictEqual(deepEqual( - { foo: ['bar', 'baz'] }, - { foo: ['bar', 'bar'] }, - ), false); - }); - - it('should compare standard types', () => { - assert.strictEqual(deepEqual( - { a: 'foo', b: false, c: -4 }, - { a: 'foo', b: false, c: -4 }, - ), true); - - assert.strictEqual(deepEqual( - { a: 'foo', b: false, c: -4 }, - { a: 'foo', b: 'false', c: -4 }, - ), false); - - assert.strictEqual(deepEqual( - { a: 'foo', b: false, c: -4 }, - { a: 'foo', b: false, c: '-4' }, - ), false); - }); -}); diff --git a/src/utils/psv.js b/src/utils/psv.js deleted file mode 100644 index 660d1ae88..000000000 --- a/src/utils/psv.js +++ /dev/null @@ -1,371 +0,0 @@ -import { LinearFilter, MathUtils, Quaternion, Texture } from 'three'; -import { PSVError } from '../PSVError'; -import { loop } from './math'; - -/** - * @summary Returns the plugin constructor from the imported object - * For retrocompatibility with previous default exports - * @memberOf PSV.utils - * @package - */ -export function pluginInterop(plugin, target) { - if (plugin) { - for (const [, p] of [['_', plugin], ...Object.entries(plugin)]) { - if (p.prototype instanceof target) { - return p; - } - } - } - return null; -} - -/** - * @summary Builds an Error with name 'AbortError' - * @memberOf PSV.utils - * @return {Error} - */ -export function getAbortError() { - const error = new Error('Loading was aborted.'); - error.name = 'AbortError'; - return error; -} - -/** - * @summary Tests if an Error has name 'AbortError' - * @memberOf PSV.utils - * @param {Error} err - * @return {boolean} - */ -export function isAbortError(err) { - return err?.name === 'AbortError'; -} - -/** - * @summary Displays a warning in the console - * @memberOf PSV.utils - * @param {string} message - */ -export function logWarn(message) { - console.warn(`PhotoSphereViewer: ${message}`); -} - -/** - * @summary Checks if an object is a {PSV.ExtendedPosition}, ie has x/y or longitude/latitude - * @memberOf PSV.utils - * @param {object} object - * @returns {boolean} - */ -export function isExtendedPosition(object) { - return [['x', 'y'], ['longitude', 'latitude']].some(([key1, key2]) => { - return object[key1] !== undefined && object[key2] !== undefined; - }); -} - -/** - * @summary Returns the value of a given attribute in the panorama metadata - * @memberOf PSV.utils - * @param {string} data - * @param {string} attr - * @returns (number) - */ -export function getXMPValue(data, attr) { - // XMP data are stored in children - let result = data.match('(.*)'); - if (result !== null) { - const val = parseInt(result[1], 10); - return isNaN(val) ? null : val; - } - - // XMP data are stored in attributes - result = data.match('GPano:' + attr + '="(.*?)"'); - if (result !== null) { - const val = parseInt(result[1], 10); - return isNaN(val) ? null : val; - } - - return null; -} - -/** - * @readonly - * @private - * @type {{top: string, left: string, bottom: string, center: string, right: string}} - */ -const CSS_POSITIONS = { - top : '0%', - bottom: '100%', - left : '0%', - right : '100%', - center: '50%', -}; - -/** - * @summary Translate CSS values like "top center" or "10% 50%" as top and left positions - * @memberOf PSV.utils - * @description The implementation is as close as possible to the "background-position" specification - * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-position} - * @param {string|PSV.Point} value - * @returns {PSV.Point} - */ -export function parsePosition(value) { - if (!value) { - return { x: 0.5, y: 0.5 }; - } - - if (typeof value === 'object') { - return value; - } - - let tokens = value.toLocaleLowerCase().split(' ').slice(0, 2); - - if (tokens.length === 1) { - if (CSS_POSITIONS[tokens[0]] !== undefined) { - tokens = [tokens[0], 'center']; - } - else { - tokens = [tokens[0], tokens[0]]; - } - } - - const xFirst = tokens[1] !== 'left' && tokens[1] !== 'right' && tokens[0] !== 'top' && tokens[0] !== 'bottom'; - - tokens = tokens.map(token => CSS_POSITIONS[token] || token); - - if (!xFirst) { - tokens.reverse(); - } - - const parsed = tokens.join(' ').match(/^([0-9.]+)% ([0-9.]+)%$/); - - if (parsed) { - return { - x: parseFloat(parsed[1]) / 100, - y: parseFloat(parsed[2]) / 100, - }; - } - else { - return { x: 0.5, y: 0.5 }; - } -} - -/** - * @readonly - * @private - */ -const X_VALUES = ['left', 'center', 'right']; -/** - * @readonly - * @private - */ -const Y_VALUES = ['top', 'center', 'bottom']; -/** - * @readonly - * @private - */ -const POS_VALUES = [...X_VALUES, ...Y_VALUES]; -/** - * @readonly - * @private - */ -const CENTER = 'center'; - -/** - * @summary Parse a CSS-like position into an array of position keywords among top, bottom, left, right and center - * @memberOf PSV.utils - * @param {string | string[]} value - * @param {object} [options] - * @param {boolean} [options.allowCenter=true] allow "center center" - * @param {boolean} [options.cssOrder=true] force CSS order (y axis then x axis) - * @return {string[]} - */ -export function cleanPosition(value, { allowCenter, cssOrder } = { allowCenter: true, cssOrder: true }) { - if (!value) { - return null; - } - - if (typeof value === 'string') { - value = value.split(' '); - } - - if (value.length === 1) { - if (value[0] === CENTER) { - value = [CENTER, CENTER]; - } - else if (X_VALUES.indexOf(value[0]) !== -1) { - value = [CENTER, value[0]]; - } - else if (Y_VALUES.indexOf(value[0]) !== -1) { - value = [value[0], CENTER]; - } - } - - if (value.length !== 2 || POS_VALUES.indexOf(value[0]) === -1 || POS_VALUES.indexOf(value[1]) === -1) { - logWarn(`Unparsable position ${value}`); - return null; - } - - if (!allowCenter && value[0] === CENTER && value[1] === CENTER) { - logWarn(`Invalid position center center`); - return null; - } - - if (cssOrder && !positionIsOrdered(value)) { - value = [value[1], value[0]]; - } - if (value[1] === CENTER && X_VALUES.indexOf(value[0]) !== -1) { - value = [CENTER, value[0]]; - } - if (value[0] === CENTER && Y_VALUES.indexOf(value[1]) !== -1) { - value = [value[1], CENTER]; - } - - return value; -} - -/** - * @summary Checks if an array of two positions is ordered (y axis then x axis) - * @param {string[]} value - * @return {boolean} - */ -export function positionIsOrdered(value) { - return Y_VALUES.indexOf(value[0]) !== -1 && X_VALUES.indexOf(value[1]) !== -1; -} - -/** - * @summary Parses an speed - * @memberOf PSV.utils - * @param {string|number} speed - The speed, in radians/degrees/revolutions per second/minute - * @returns {number} radians per second - * @throws {PSV.PSVError} when the speed cannot be parsed - */ -export function parseSpeed(speed) { - let parsed; - - if (typeof speed === 'string') { - const speedStr = speed.toString().trim(); - - // Speed extraction - let speedValue = parseFloat(speedStr.replace(/^(-?[0-9]+(?:\.[0-9]*)?).*$/, '$1')); - const speedUnit = speedStr.replace(/^-?[0-9]+(?:\.[0-9]*)?(.*)$/, '$1').trim(); - - // "per minute" -> "per second" - if (speedUnit.match(/(pm|per minute)$/)) { - speedValue /= 60; - } - - // Which unit? - switch (speedUnit) { - // Degrees per minute / second - case 'dpm': - case 'degrees per minute': - case 'dps': - case 'degrees per second': - parsed = MathUtils.degToRad(speedValue); - break; - - // Radians per minute / second - case 'rdpm': - case 'radians per minute': - case 'rdps': - case 'radians per second': - parsed = speedValue; - break; - - // Revolutions per minute / second - case 'rpm': - case 'revolutions per minute': - case 'rps': - case 'revolutions per second': - parsed = speedValue * Math.PI * 2; - break; - - // Unknown unit - default: - throw new PSVError('Unknown speed unit "' + speedUnit + '"'); - } - } - else { - parsed = speed; - } - - return parsed; -} - -/** - * @summary Parses an angle value in radians or degrees and returns a normalized value in radians - * @memberOf PSV.utils - * @param {string|number} angle - eg: 3.14, 3.14rad, 180deg - * @param {boolean} [zeroCenter=false] - normalize between -Pi - Pi instead of 0 - 2*Pi - * @param {boolean} [halfCircle=zeroCenter] - normalize between -Pi/2 - Pi/2 instead of -Pi - Pi - * @returns {number} - * @throws {PSV.PSVError} when the angle cannot be parsed - */ -export function parseAngle(angle, zeroCenter = false, halfCircle = zeroCenter) { - let parsed; - - if (typeof angle === 'string') { - const match = angle.toLowerCase().trim().match(/^(-?[0-9]+(?:\.[0-9]*)?)(.*)$/); - - if (!match) { - throw new PSVError('Unknown angle "' + angle + '"'); - } - - const value = parseFloat(match[1]); - const unit = match[2]; - - if (unit) { - switch (unit) { - case 'deg': - case 'degs': - parsed = MathUtils.degToRad(value); - break; - case 'rad': - case 'rads': - parsed = value; - break; - default: - throw new PSVError('Unknown angle unit "' + unit + '"'); - } - } - else { - parsed = value; - } - } - else if (typeof angle === 'number' && !isNaN(angle)) { - parsed = angle; - } - else { - throw new PSVError('Unknown angle "' + angle + '"'); - } - - parsed = loop(zeroCenter ? parsed + Math.PI : parsed, Math.PI * 2); - - return zeroCenter ? MathUtils.clamp(parsed - Math.PI, -Math.PI / (halfCircle ? 2 : 1), Math.PI / (halfCircle ? 2 : 1)) : parsed; -} - -/** - * @summary Creates a THREE texture from an image - * @memberOf PSV.utils - * @param {HTMLImageElement | HTMLCanvasElement} img - * @return {external:THREE.Texture} - */ -export function createTexture(img) { - const texture = new Texture(img); - texture.needsUpdate = true; - texture.minFilter = LinearFilter; - texture.generateMipmaps = false; - return texture; -} - -const quaternion = new Quaternion(); - -/** - * @summary Applies the inverse of Euler angles to a vector - * @memberOf PSV.utils - * @param {external:THREE.Vector3} vector - * @param {external:THREE.Euler} euler - */ -export function applyEulerInverse(vector, euler) { - quaternion.setFromEuler(euler).invert(); - vector.applyQuaternion(quaternion); -} diff --git a/src/utils/psv.spec.js b/src/utils/psv.spec.js deleted file mode 100644 index aa4265ba2..000000000 --- a/src/utils/psv.spec.js +++ /dev/null @@ -1,353 +0,0 @@ -import assert from 'assert'; - -import { cleanPosition, getXMPValue, parseAngle, parsePosition, parseSpeed } from './psv'; - -describe('utils:psv:parseAngle', () => { - it('should normalize number', () => { - assert.strictEqual(parseAngle(0), 0, '0'); - assert.strictEqual(parseAngle(Math.PI), Math.PI, 'PI'); - assert.strictEqual(parseAngle(3 * Math.PI), Math.PI, '3xPI'); - - assert.strictEqual(parseAngle(0, true), 0, '0 centered'); - assert.strictEqual(parseAngle(Math.PI * 3 / 4, true), Math.PI / 2, '3/4xPI centered'); - assert.strictEqual(parseAngle(-Math.PI * 3 / 4, true), -Math.PI / 2, '-3/4xPI centered'); - }); - - it('should parse radians angles', () => { - const values = { - '0' : 0, - '1.72' : 1.72, - '-2.56' : Math.PI * 2 - 2.56, - '3.14rad' : 3.14, - '-3.14rad': Math.PI * 2 - 3.14 - }; - - for (const pos in values) { - assert.strictEqual(parseAngle(pos).toFixed(16), values[pos].toFixed(16), pos); - } - }); - - it('should parse degrees angles', () => { - const values = { - '0deg' : 0, - '30deg' : 30 * Math.PI / 180, - '-30deg' : Math.PI * 2 - 30 * Math.PI / 180, - '85degs' : 85 * Math.PI / 180, - '360degs': 0 - }; - - for (const pos in values) { - assert.strictEqual(parseAngle(pos).toFixed(16), values[pos].toFixed(16), pos); - } - }); - - it('should normalize angles between 0 and 2Pi', () => { - const values = { - '450deg' : Math.PI / 2, - '1440deg': 0, - '8.15' : 8.15 - Math.PI * 2, - '-3.14' : Math.PI * 2 - 3.14, - '-360deg': 0 - }; - - for (const pos in values) { - assert.strictEqual(parseAngle(pos).toFixed(16), values[pos].toFixed(16), pos); - } - }); - - it('should normalize angles between -Pi/2 and Pi/2', () => { - const values = { - '45deg': Math.PI / 4, - '-4' : Math.PI / 2 - }; - - for (const pos in values) { - assert.strictEqual(parseAngle(pos, true).toFixed(16), values[pos].toFixed(16), pos); - } - }); - - it('should normalize angles between -Pi and Pi', function () { - const values = { - '45deg': Math.PI / 4, - '4' : -2 * Math.PI + 4 - }; - - for (const pos in values) { - assert.strictEqual(parseAngle(pos, true, false).toFixed(16), values[pos].toFixed(16), pos); - } - }); - - it('should throw exception on invalid values', () => { - assert.throws(() => { - parseAngle('foobar'); - }, /Unknown angle "foobar"/, 'foobar'); - - assert.throws(() => { - parseAngle('200gr') - }, /Unknown angle unit "gr"/, '200gr'); - }); -}); - - -describe('utils:psv:parsePosition', () => { - it('should parse 2 keywords', () => { - const values = { - 'top left' : { x: 0, y: 0 }, - 'top center' : { x: 0.5, y: 0 }, - 'top right' : { x: 1, y: 0 }, - 'center left' : { x: 0, y: 0.5 }, - 'center center': { x: 0.5, y: 0.5 }, - 'center right' : { x: 1, y: 0.5 }, - 'bottom left' : { x: 0, y: 1 }, - 'bottom center': { x: 0.5, y: 1 }, - 'bottom right' : { x: 1, y: 1 } - }; - - for (const pos in values) { - assert.deepStrictEqual(parsePosition(pos), values[pos], pos); - - const rev = pos.split(' ').reverse().join(' '); - assert.deepStrictEqual(parsePosition(rev), values[pos], rev); - } - }); - - it('should parse 1 keyword', () => { - const values = { - 'top' : { x: 0.5, y: 0 }, - 'center': { x: 0.5, y: 0.5 }, - 'bottom': { x: 0.5, y: 1 }, - 'left' : { x: 0, y: 0.5 }, - 'right' : { x: 1, y: 0.5 }, - }; - - for (const pos in values) { - assert.deepStrictEqual(parsePosition(pos), values[pos], pos); - } - }); - - it('should parse 2 percentages', () => { - const values = { - '0% 0%' : { x: 0, y: 0 }, - '50% 50%' : { x: 0.5, y: 0.5 }, - '100% 100%': { x: 1, y: 1 }, - '10% 80%' : { x: 0.1, y: 0.8 }, - '80% 10%' : { x: 0.8, y: 0.1 } - }; - - for (const pos in values) { - assert.deepStrictEqual(parsePosition(pos), values[pos], pos); - } - }); - - it('should parse 1 percentage', () => { - const values = { - '0%' : { x: 0, y: 0 }, - '50%' : { x: 0.5, y: 0.5 }, - '100%': { x: 1, y: 1 }, - '80%' : { x: 0.8, y: 0.8 } - }; - - for (const pos in values) { - assert.deepStrictEqual(parsePosition(pos), values[pos], pos); - } - }); - - it('should parse mixed keyword & percentage', () => { - const values = { - 'top 80%' : { x: 0.8, y: 0 }, - '80% bottom': { x: 0.8, y: 1 }, - 'left 40%' : { x: 0, y: 0.4 }, - '40% right' : { x: 1, y: 0.4 }, - 'center 10%': { x: 0.5, y: 0.1 }, - '10% center': { x: 0.1, y: 0.5 } - }; - - for (const pos in values) { - assert.deepStrictEqual(parsePosition(pos), values[pos], pos); - } - }); - - it('should fallback on parse fail', () => { - const values = { - '' : { x: 0.5, y: 0.5 }, - 'crap' : { x: 0.5, y: 0.5 }, - 'foo bar': { x: 0.5, y: 0.5 }, - 'foo 50%': { x: 0.5, y: 0.5 }, - '%' : { x: 0.5, y: 0.5 } - }; - - for (const pos in values) { - assert.deepStrictEqual(parsePosition(pos), values[pos], pos); - } - }); - - it('should ignore extra tokens', () => { - const values = { - 'top center bottom' : { x: 0.5, y: 0 }, - '50% left 20%' : { x: 0, y: 0.5 }, - '0% 0% okay this time it goes ridiculous': { x: 0, y: 0 } - }; - - for (const pos in values) { - assert.deepStrictEqual(parsePosition(pos), values[pos], pos); - } - }); - - it('should ignore case', () => { - const values = { - 'TOP CENTER' : { x: 0.5, y: 0 }, - 'cenTer LefT': { x: 0, y: 0.5 } - }; - - for (const pos in values) { - assert.deepStrictEqual(parsePosition(pos), values[pos], pos); - } - }); -}); - - -describe('utils:psv:parseSpeed', () => { - it('should parse all units', () => { - const values = { - '360dpm' : 360 * Math.PI / 180 / 60, - '360degrees per minute' : 360 * Math.PI / 180 / 60, - '10dps' : 10 * Math.PI / 180, - '10degrees per second' : 10 * Math.PI / 180, - '2radians per minute' : 2 / 60, - '0.1radians per second' : 0.1, - '2rpm' : 2 * 2 * Math.PI / 60, - '2revolutions per minute' : 2 * 2 * Math.PI / 60, - '0.01rps' : 0.01 * 2 * Math.PI, - '0.01revolutions per second': 0.01 * 2 * Math.PI - }; - - for (const speed in values) { - assert.strictEqual(parseSpeed(speed).toFixed(16), values[speed].toFixed(16), speed); - } - }); - - it('should allow various forms', () => { - const values = { - '2rpm' : 2 * 2 * Math.PI / 60, - '2 rpm' : 2 * 2 * Math.PI / 60, - '2revolutions per minute' : 2 * 2 * Math.PI / 60, - '2 revolutions per minute' : 2 * 2 * Math.PI / 60, - '-2rpm' : -2 * 2 * Math.PI / 60, - '-2 rpm' : -2 * 2 * Math.PI / 60, - '-2revolutions per minute' : -2 * 2 * Math.PI / 60, - '-2 revolutions per minute': -2 * 2 * Math.PI / 60 - }; - - for (const speed in values) { - assert.strictEqual(parseSpeed(speed).toFixed(16), values[speed].toFixed(16), speed); - } - }); - - it('should throw exception on invalid unit', () => { - assert.throws(() => { - parseSpeed('10rpsec'); - }, /Unknown speed unit "rpsec"/, '10rpsec'); - }); - - it('should passthrough when number', () => { - assert.strictEqual(parseSpeed(Math.PI), Math.PI); - }); -}); - -describe('utils:psv:getXMPValue', () => { - it('should parse XMP data with children', () => { - const data = '\ - equirectangular\ - True\ - 5376\ - 2688\ - 5376\ - 2688\ - 0\ - 0\ - 270.0\ - 0\ - 0.2\ - '; - - assert.deepStrictEqual([ - getXMPValue(data, 'FullPanoWidthPixels'), - getXMPValue(data, 'FullPanoHeightPixels'), - getXMPValue(data, 'CroppedAreaImageWidthPixels'), - getXMPValue(data, 'CroppedAreaImageHeightPixels'), - getXMPValue(data, 'CroppedAreaLeftPixels'), - getXMPValue(data, 'CroppedAreaTopPixels'), - getXMPValue(data, 'PoseHeadingDegrees'), - getXMPValue(data, 'PosePitchDegrees'), - getXMPValue(data, 'PoseRollDegrees'), - ], [ - 5376, 2688, 5376, 2688, 0, 0, 270, 0, 0 - ]) - }); - - it('should parse XMP data with attributes', () => { - const data = ''; - - assert.deepStrictEqual([ - getXMPValue(data, 'FullPanoWidthPixels'), - getXMPValue(data, 'FullPanoHeightPixels'), - getXMPValue(data, 'CroppedAreaImageWidthPixels'), - getXMPValue(data, 'CroppedAreaImageHeightPixels'), - getXMPValue(data, 'CroppedAreaLeftPixels'), - getXMPValue(data, 'CroppedAreaTopPixels'), - getXMPValue(data, 'PoseHeadingDegrees'), - getXMPValue(data, 'PosePitchDegrees'), - getXMPValue(data, 'PoseRollDegrees'), - ], [ - 5376, 2688, 5376, 2688, 0, 0, 270, 0, 0 - ]) - }); - -}); - -describe('utils:psv:cleanPosition', () => { - it('should clean various formats', () => { - assert.deepStrictEqual(cleanPosition('top right'), ['top', 'right']); - assert.deepStrictEqual(cleanPosition('right top'), ['top', 'right']); - assert.deepStrictEqual(cleanPosition(['top', 'right']), ['top', 'right']); - }); - - it('should add missing center', () => { - assert.deepStrictEqual(cleanPosition('top'), ['top', 'center']); - assert.deepStrictEqual(cleanPosition('left'), ['center', 'left']); - assert.deepStrictEqual(cleanPosition('center'), ['center', 'center']); - }); - - it('should dissallow all center', () => { - assert.strictEqual(cleanPosition('center center', { allowCenter: false }), null); - assert.strictEqual(cleanPosition('center', { allowCenter: false }), null); - }); - - it('should return null on unparsable values', () => { - assert.strictEqual(cleanPosition('foo bar'), null); - assert.strictEqual(cleanPosition('TOP CENTER'), null); - assert.strictEqual(cleanPosition(''), null); - assert.strictEqual(cleanPosition(undefined), null); - }); - - it('should allow XY order', () => { - assert.deepStrictEqual(cleanPosition('right top', { cssOrder: false }), ['right', 'top']); - assert.deepStrictEqual(cleanPosition(['top', 'right'], { cssOrder: false }), ['top', 'right']); - }); - - it('should always order with center', () => { - assert.deepStrictEqual(cleanPosition('center top'), ['top', 'center']); - assert.deepStrictEqual(cleanPosition('left center'), ['center', 'left']); - }); -}); diff --git a/tests/package.json b/tests/package.json deleted file mode 100644 index a691b573c..000000000 --- a/tests/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "photo-sphere-viewer-tests", - "version": "1.0.0", - "license": "MIT", - "private": true, - "scripts": { - "pretest": "cpx \"../dist/**\" \"node_modules/photo-sphere-viewer/dist\" && cpx \"../package.json\" \"node_modules/photo-sphere-viewer\"", - "test": "tsc types/index.ts" - } -} diff --git a/tests/types/.gitignore b/tests/types/.gitignore deleted file mode 100644 index a6c7c2852..000000000 --- a/tests/types/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.js diff --git a/tests/types/CustomPlugin.ts b/tests/types/CustomPlugin.ts deleted file mode 100644 index 7314512d6..000000000 --- a/tests/types/CustomPlugin.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { AbstractPlugin, Viewer } from 'photo-sphere-viewer'; - -export class CustomPlugin extends AbstractPlugin { - - static id = 'custom'; - - constructor(psv: Viewer) { - super(psv); - } - - doSomething() { - - } - -} diff --git a/tests/types/index.ts b/tests/types/index.ts deleted file mode 100644 index cbca044b9..000000000 --- a/tests/types/index.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { CONSTANTS, utils, Viewer } from 'photo-sphere-viewer'; -import { - EquirectangularTilesAdapter, - EquirectangularTilesPanorama -} from 'photo-sphere-viewer/dist/adapters/equirectangular-tiles'; -import { EVENTS as MAKER_EVENTS, MarkersPlugin, MarkersPluginOptions } from 'photo-sphere-viewer/dist/plugins/markers'; -import { CustomPlugin } from './CustomPlugin'; - -const viewer = new Viewer({ - container: 'container', - adapter: EquirectangularTilesAdapter, - plugins: [ - [MarkersPlugin, { - clickEventOnMarker: true, - } as MarkersPluginOptions], - CustomPlugin, - ], -}); - -viewer.setPanorama({ - baseUrl: 'small.jpg', - width: 16000, - cols: 8, - rows: 4, -} as EquirectangularTilesPanorama, { - transition: false, -}) - .then(() => { - - }); - -viewer.animate({ - longitude: 0, - latitude: 0, - speed: '2rpm', -}) - .then(() => { - - }); - -viewer.zoom(50); - -viewer.setOption('useXmpData', true); - -viewer.setOptions({ - useXmpData: false, -}); - -viewer.navbar.setCaption('Test'); - -viewer.panel.show({ - content: 'Content', - clickHandler: (e: MouseEvent) => null, -}); - -viewer.once('ready', e => { - -}); - -viewer.on('position-updated', (e, position) => { - const longitude: number = position.longitude; -}); - -viewer.on(CONSTANTS.CHANGE_EVENTS.GET_ANIMATE_POSITION, (e, position) => { - return {longitude: position.longitude + 0.1, latitude: position.latitude + 0.1}; -}); - -const markers = viewer.getPlugin(MarkersPlugin); -markers.on('select-marker', (e, marker) => { - const markerId: string = marker.id; -}); -markers.on(MAKER_EVENTS.UNSELECT_MARKER, (e, marker) => { - const markerId: string = marker.id; -}); -markers.on(MarkersPlugin.EVENTS.UNSELECT_MARKER, (e, marker) => { - const markerId: string = marker.id; -}); - -const customPlugin = viewer.getPlugin(CustomPlugin); -customPlugin.doSomething(); - -const customPluginAgain = viewer.getPlugin('custom'); -customPluginAgain.doSomething(); - -const anim = new utils.Animation({ - duration: 1000, - properties: { - foo: {start: 0, end: 1}, - }, - onTick: (properties) => { - console.log(properties.foo); - } -}); - -anim.then(completed => console.log(completed)); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..7f0e0f5d6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "es2021", + "composite": false, + "declaration": true, + "declarationMap": false, + "stripInternal": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "strictNullChecks": false, + "lib": ["es2021", "dom"] + }, + "typedocOptions": { + "name": "Photo Sphere Viewer", + "includeVersion": false, + "githubPages": false, + "cleanOutputDir": false, + "excludePrivate": true, + "entryPointStrategy": "packages", + "readme": "none", + "gaID": "UA-28192323-3", + "entryPoints": ["packages/*"], + "validation": { + "notExported": false + }, + "navigationLinks": { + "📃 Main documentation": "https://photo-sphere-viewer.js.org" + }, + "visibilityFilters": { + "protected": true, + "inherited": false + }, + "favicon": "docs/.vuepress/public/favicon.png", + "customCss": "docs/.typedoc/style.css", + "footerLastModified": true, + "externalSymbolLinkMappings": { + "typescript": { + "Error": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error", + "Event": "https://developer.mozilla.org/docs/Web/API/Event", + "EventTarget": "https://developer.mozilla.org/docs/Web/API/EventTarget" + }, + "@types/three": { + "*": "https://threejs.org/docs" + } + } + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 000000000..561c3ae76 --- /dev/null +++ b/turbo.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://turborepo.org/schema.json", + "globalDependencies": [ + "build/**", + "tsconfig.json", + ".eslintrc.json", + ".stylelintrc.json" + ], + "pipeline": { + "build": { + "dependsOn": ["^build"], + "inputs": ["src/**"], + "outputs": ["dist/**"] + }, + "lint": { + "inputs": ["src/**"], + "outputs": [] + }, + "test": { + "inputs": ["src/**"], + "outputs": [] + }, + "watch": { + "cache": false + }, + "//#watch": { + "cache": false + }, + "publish-dist": { + "dependsOn": ["^publish-dist"], + "cache": false + } + } +} diff --git a/types/PSVError.d.ts b/types/PSVError.d.ts deleted file mode 100644 index 7300a10b1..000000000 --- a/types/PSVError.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * @summary Custom error used in the lib - */ -export class PSVError extends Error { - name: 'PSVError'; -} diff --git a/types/Viewer.d.ts b/types/Viewer.d.ts deleted file mode 100644 index bb95c2637..000000000 --- a/types/Viewer.d.ts +++ /dev/null @@ -1,414 +0,0 @@ -import { Vector3 } from 'three'; -import { Event, EventEmitter } from 'uevent'; -import { AdapterConstructor } from './adapters/AbstractAdapter'; -import { Animation } from './utils/Animation'; -import { Loader } from './components/Loader'; -import { Navbar } from './components/Navbar'; -import { Notification } from './components/Notification'; -import { Overlay } from './components/Overlay'; -import { Panel } from './components/Panel'; -import { Tooltip } from './components/Tooltip'; -import { - AnimateOptions, - ClickData, - CssSize, - ExtendedPosition, - NavbarCustomButton, - PanoData, - PanoDataProvider, - PanoramaOptions, - Position, - Size, - TextureData -} from './models'; -import { AbstractPlugin, PluginConstructor } from './plugins/AbstractPlugin'; -import { DataHelper } from './services/DataHelper'; -import { TextureLoader } from './services/TextureLoader'; -import { TooltipRenderer } from './services/TooltipRenderer'; - -/** - * @summary Viewer options, see {@link http://photo-sphere-viewer.js.org/guide/config.html} - */ -export type ViewerOptions = { - container: HTMLElement | string; - panorama?: any; - adapter?: AdapterConstructor | [AdapterConstructor, any]; - overlay?: any; - overlayOpacity?: number; - caption?: string; - description?: string; - downloadUrl?: string; - loadingImg?: string; - loadingTxt?: string; - size?: Size; - fisheye?: boolean | number; - minFov?: number; - maxFov?: number; - defaultZoomLvl?: number; - defaultLong?: number; - defaultLat?: number; - sphereCorrection?: { pan?: number, tilt?: number, roll?: number }; - moveSpeed?: number; - zoomSpeed?: number; - autorotateDelay?: number, - autorotateIdle?: boolean; - autorotateSpeed?: string | number; - autorotateLat?: number; - autorotateZoomLvl?: number; - moveInertia?: boolean; - mousewheel?: boolean; - mousemove?: boolean; - /** - * @deprecated - */ - captureCursor?: boolean; - mousewheelCtrlKey?: boolean; - touchmoveTwoFingers?: boolean; - useXmpData?: boolean; - panoData?: PanoData | PanoDataProvider; - requestHeaders?: Record | ((url: string) => Record); - canvasBackground?: string; - withCredentials?: boolean; - navbar?: string | Array; - lang?: Record; - keyboard?: Record; - plugins?: Array | [PluginConstructor, any]>; -}; - -/** - * Internal properties of the viewer - */ -export type ViewerProps = { - ready: boolean; - uiRefresh: boolean; - needsUpdate: boolean; - fullscreen: boolean; - direction: Vector3; - vFov: number; - hFov: number; - aspect: number; - autorotateEnabled: boolean; - animationPromise: Animation; - loadingPromise: Promise; - startTimeout: any; - size: Size; - panoData: PanoData; -}; - -/** - * Main class - */ -export class Viewer extends EventEmitter { - - /** - * Configuration holder - */ - readonly config: ViewerOptions; - - /** - * Internal properties - */ - protected readonly prop: ViewerProps; - - /** - * Top most parent - */ - readonly parent: HTMLElement; - - /** - * Main container - */ - readonly container: HTMLElement; - - /** - * Textures loader - */ - readonly textureLoader: TextureLoader; - - /** - * Utilities to help converting data - */ - readonly dataHelper: DataHelper; - - readonly loader: Loader; - - readonly navbar: Navbar; - - readonly panel: Panel; - - readonly tooltip: TooltipRenderer; - - readonly notification: Notification; - - readonly overlay: Overlay; - - /** - * @throws {PSVError} when the configuration is incorrect - */ - constructor(options: ViewerOptions); - - /** - * @summary Destroys the viewer - * @description The memory used by the ThreeJS context is not totally cleared. This will be fixed as soon as possible. - */ - destroy(); - - /** - * @summary Returns the instance of a plugin if it exists - */ - getPlugin(pluginId: string | PluginConstructor): T | undefined; - - /** - * @summary Returns the current position of the camera - */ - getPosition(): Position; - - /** - * @summary Returns the current zoom level - */ - getZoomLevel(): number; - - /** - * @summary Returns the current viewer size - */ - getSize(): Size; - - /** - * @summary Checks if the automatic rotation is enabled - */ - isAutorotateEnabled(): boolean; - - /** - * @summary Checks if the viewer is in fullscreen - */ - isFullscreenEnabled(): boolean; - - /** - * @summary Flags the view has changed for the next render - */ - needsUpdate(); - - /** - * @summary Resizes the canvas when the window is resized - */ - autoSize(); - - /** - * @summary Loads a new panorama file - * @description Loads a new panorama file, optionally changing the camera position/zoom and activating the transition animation.
- * If the "options" parameter is not defined, the camera will not move and the ongoing animation will continue.
- * If another loading is already in progress it will be aborted. - * @returns resolves false if the loading was aborted by another call - */ - setPanorama(panorama: any, options?: PanoramaOptions): Promise; - - /** - * @summary Loads a new overlay - */ - setOverlay(path: any, opacity?: number): Promise; - - /** - * @summary Update options - */ - setOptions(options: Partial); - - /** - * @summary Update options - */ - setOption(option: K, value: ViewerOptions[K]); - - /** - * @summary Starts the automatic rotation - */ - startAutorotate(); - - /** - * @summary Stops the automatic rotation - */ - stopAutorotate(); - - /** - * @summary Starts or stops the automatic rotation - */ - toggleAutorotate(); - - /** - * @summary Displays an error message - */ - showError(message: string); - - /** - * @summary Hides the error message - */ - hideError(); - - /** - * @summary Rotates the view to specific longitude and latitude - */ - rotate(position: ExtendedPosition); - - /** - * @summary Rotates and zooms the view with a smooth animation - */ - animate(options: AnimateOptions): Animation; - - /** - * @summary Stops the ongoing animation - * @description The return value is a Promise because the is no guaranty the animation can be stopped synchronously. - */ - stopAnimation(): Promise; - - /** - * @summary Zooms to a specific level between `max_fov` and `min_fov` - */ - zoom(level: number); - - /** - * @summary Increases the zoom level - * @param {number} [step=1] - */ - zoomIn(step?: number); - - /** - * @summary Decreases the zoom level - * @param {number} [step=1] - */ - zoomOut(step?: number); - - /** - * @summary Resizes the viewer - */ - resize(size: CssSize); - - /** - * @summary Enters the fullscreen mode - */ - enterFullscreen(); - - /** - * @summary Exits the fullscreen mode - */ - exitFullscreen(); - - /** - * @summary Enters or exits the fullscreen mode - */ - toggleFullscreen(); - - /** - * @summary Enables the keyboard controls (done automatically when entering fullscreen) - */ - startKeyboardControl(); - - /** - * @summary Disables the keyboard controls (done automatically when exiting fullscreen) - */ - stopKeyboardControl(); - - /** - * @summary Triggered when the panorama image has been loaded and the viewer is ready to perform the first render - */ - once(e: 'ready', cb: (e: Event) => void): this; - - /** - * @summary Triggered when the automatic rotation is enabled/disabled - */ - on(e: 'autorotate', cb: (e: Event, enabled: true) => void): this; - /** - * @summary Triggered before a render, used to modify the view - */ - on(e: 'before-render', cb: (e: Event, timestamp: number, elapsed: number) => void): this; - /** - * @summary Triggered before a rotate operation, can be cancelled - */ - on(e: 'before-rotate', cb: (e: Event, position: ExtendedPosition) => void): this; - /** - * @summary Triggered when the user clicks on the viewer (everywhere excluding the navbar and the side panel) - */ - on(e: 'click', cb: (e: Event, data: ClickData) => void): this; - /** - * @summary Trigered when the panel is closed - */ - on(e: 'close-panel', cb: (e: Event, id: string | undefined) => void): this; - /** - * @summary Triggered after a call to setOption/setOptions - */ - on(e: 'config-changed', cb: (e: Event, options: string[]) => void): this; - /** - * @summary Triggered when the user double clicks on the viewer. The simple `click` event is always fired before `dblclick` - */ - on(e: 'dblclick', cb: (e: Event, data: ClickData) => void): this; - /** - * @summary Triggered when the fullscreen mode is enabled/disabled - */ - on(e: 'fullscreen-updated', cb: (e: Event, enabled: true) => void): this; - /** - * @summary Called to alter the target position of an animation - */ - on(e: 'get-animate-position', cb: (e: Event, position: Position) => Position): this; - /** - * @summary Called to alter the target position of a rotation - */ - on(e: 'get-rotate-position', cb: (e: Event, position: Position) => Position): this; - /** - * @summary Triggered when the notification is hidden - */ - on(e: 'hide-notification', cb: (e: Event, id: string | undefined) => void): this; - /** - * @summary Triggered when the overlay is hidden - */ - on(e: 'hide-overlay', cb: (e: Event, id: string | undefined) => void): this; - /** - * @summary Triggered when the tooltip is hidden - */ - on(e: 'hide-tooltip', cb: (e: Event, data: any) => void): this; - /** - * @summary Triggered when a key is pressed, can be cancelled - */ - on(e: 'key-press', cb: (e: Event, key: string) => void): this; - /** - * @summary Triggered when the loader value changes - */ - on(e: 'load-progress', cb: (e: Event, value: number) => void): this; - /** - * @summary Triggered when the panel is opened - */ - on(e: 'open-panel', cb: (e: Event, id: string | undefined) => void): this; - /** - * @summary Triggered when a panorama image has been loaded - */ - on(e: 'panorama-loaded', cb: (e: Event, textureData: TextureData) => void): this; - /** - * @summary Triggered when the view longitude and/or latitude changes - */ - on(e: 'position-updated', cb: (e: Event, position: Position) => void): this; - /** - * @summary Triggered on each viewer render, **this event is triggered very often** - */ - on(e: 'render', cb: (e: Event) => void): this; - /** - * @summary Trigered when the notification is shown - */ - on(e: 'show-notification', cb: (e: Event, id: string | undefined) => void): this; - /** - * @summary Trigered when the overlay is shown - */ - on(e: 'show-overlay', cb: (e: Event, id: string | undefined) => void): this; - /** - * @summary Trigered when the tooltip is shown - */ - on(e: 'show-tooltip', cb: (e: Event, data: any, tooltip: Tooltip) => void): this; - /** - * @summary Triggered when the viewer size changes - */ - on(e: 'size-updated', cb: (e: Event, size: Size) => void): this; - /** - * @summary Triggered when all current animations are stopped - */ - on(e: 'stop-all', cb: (e: Event) => void): this; - /** - * @summary Triggered when the zoom level changes - */ - on(e: 'zoom-updated', cb: (e: Event, zoom: number) => void): this; - -} diff --git a/types/adapters/AbstractAdapter.d.ts b/types/adapters/AbstractAdapter.d.ts deleted file mode 100644 index b947feaed..000000000 --- a/types/adapters/AbstractAdapter.d.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Mesh } from 'three'; -import { PanoData, PanoDataProvider, TextureData } from '../models'; -import { Viewer } from '../Viewer'; - -/** - * @summary Base adapters class - * @template T type of the panorama configuration object - */ -export abstract class AbstractAdapter { - - /** - * @summary Unique identifier of the adapter - */ - static id: string; - - /** - * @summary Indicates if the adapter supports panorama download natively - */ - static supportsDownload: boolean; - - /** - * @summary Indicated if the adapter can display an additional transparent image above the panorama - */ - static supportsOverlay: boolean; - - constructor(parent: Viewer); - - /** - * @summary Destroys the adapter - */ - destroy(); - - /** - * @summary Indicates if the adapter supports transitions between panoramas - */ - supportsTransition(panorama: T): boolean; - - /** - * @summary Indicates if the adapter supports preload of a panorama - */ - supportsPreload(panorama: T): boolean; - - /** - * @summary Loads the panorama texture(s) - */ - loadTexture(panorama: T, newPanoData?: PanoData | PanoDataProvider, useXmpPanoData?: boolean): Promise; - - /** - * @summary Creates the cube mesh - * @param {number} [scale=1] - */ - createMesh(scale?: number): Mesh; - - /** - * @summary Applies the texture to the mesh - */ - setTexture(mesh: Mesh, textureData: TextureData, transition?: boolean); - - /** - * @summary Changes the opacity of the mesh - */ - setTextureOpacity(mesh: Mesh, opacity: number); - - /** - * @summary Cleanup a loaded texture, used on load abort - */ - disposeTexture(textureData: TextureData); - - /** - * @summary Applies the overlay to the mesh - */ - setOverlay(mesh: Mesh, textureData: TextureData, opacity: number); - -} - -export type AdapterConstructor> = new (psv: Viewer, options?: any) => T; diff --git a/types/adapters/cubemap-tiles/index.d.ts b/types/adapters/cubemap-tiles/index.d.ts deleted file mode 100644 index 548aafd3b..000000000 --- a/types/adapters/cubemap-tiles/index.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { AbstractAdapter, Viewer } from '../..'; -import { Cubemap, CubemapArray } from '../cubemap'; - -/** - * @summary Configuration of a tiled cubemap - */ -export type CubemapTilesPanorama = { - baseUrl?: CubemapArray | Cubemap; - faceSize: number; - nbTiles: number; - tileUrl: (face: keyof Cubemap, col: number, row: number) => string; -}; - -export type CubemapTilesAdapterOptions = { - flipTopBottom?: boolean; - showErrorTile?: boolean; - baseBlur?: boolean; -} - -/** - * @summary Adapter for tiled cubemaps - */ -export class CubemapTilesAdapter extends AbstractAdapter { - - constructor(psv: Viewer, options: CubemapTilesAdapterOptions); - -} diff --git a/types/adapters/cubemap-video/index.d.ts b/types/adapters/cubemap-video/index.d.ts deleted file mode 100644 index eaa4104bd..000000000 --- a/types/adapters/cubemap-video/index.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AbstractAdapter, Viewer } from '../..'; - -/** - * @summary Configuration of a cubemap video - */ -export type CubemapVideoPanorama = { - source: string; -}; - -export type CubemapVideoAdapterOptions = { - autoplay?: boolean; - muted?: boolean; - equiangular?: boolean; -} - -/** - * @summary Adapter for cubemap videos - */ -export class CubemapVideoAdapter extends AbstractAdapter { - - constructor(psv: Viewer, options: CubemapVideoAdapterOptions); - -} diff --git a/types/adapters/cubemap/index.d.ts b/types/adapters/cubemap/index.d.ts deleted file mode 100644 index b16e17078..000000000 --- a/types/adapters/cubemap/index.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { AbstractAdapter, Viewer } from '../..'; - -/** - * @summary Cubemap defined as an array of images - * @description images order is : left, front, right, back, top, bottom - */ -export type CubemapArray = string[6]; - -/** - * @summary Object defining a cubemap - */ -export type Cubemap = { - left: string; - front: string; - right: string; - back: string; - top: string; - bottom: string; -}; - -export type CubemapAdapterOptions = { - flipTopBottom?: boolean; -}; - -/** - * @summary Adapter for cubemaps - */ -export class CubemapAdapter extends AbstractAdapter { - - constructor(psv: Viewer, options: CubemapAdapterOptions); - -} diff --git a/types/adapters/equirectangular-tiles/index.d.ts b/types/adapters/equirectangular-tiles/index.d.ts deleted file mode 100644 index c67272de5..000000000 --- a/types/adapters/equirectangular-tiles/index.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { AbstractAdapter, Viewer, PanoData, PanoDataProvider } from '../..'; - -/** - * @summary Configuration of a tiled panorama - */ -export type EquirectangularTilesPanorama = { - baseUrl?: string; - basePanoData?: PanoData | PanoDataProvider; - width: number; - cols: number; - rows: number; - tileUrl: (col: number, row: number) => string; -}; - -export type EquirectangularTilesAdapterOptions = { - resolution?: number, - showErrorTile?: boolean; - baseBlur?: boolean; -}; - -/** - * @summary Adapter for tiled panoramas - */ -export class EquirectangularTilesAdapter extends AbstractAdapter { - - constructor(psv: Viewer, options: EquirectangularTilesAdapterOptions); - -} diff --git a/types/adapters/equirectangular-video/index.d.ts b/types/adapters/equirectangular-video/index.d.ts deleted file mode 100644 index 6a726ec42..000000000 --- a/types/adapters/equirectangular-video/index.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { AbstractAdapter, Viewer } from '../..'; - -/** - * @summary Configuration of an equirectangular video - */ -export type EquirectangularVideoPanorama = { - source: string; -}; - -export type EquirectangularVideoAdapterOptions = { - autoplay?: boolean; - muted?: boolean; - resolution?: number; -} - -/** - * @summary Adapter for equirectangular videos - */ -export class EquirectangularVideoAdapter extends AbstractAdapter { - - constructor(psv: Viewer, options: EquirectangularVideoAdapterOptions); - -} diff --git a/types/adapters/equirectangular/index.d.ts b/types/adapters/equirectangular/index.d.ts deleted file mode 100644 index f8f17b1c7..000000000 --- a/types/adapters/equirectangular/index.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AbstractAdapter, Viewer } from '../..'; - -export type EquirectangularAdapterOptions = { - resolution?: number, -}; - -/** - * @summary Adapter for equirectangular panoramas - */ -export class EquirectangularAdapter extends AbstractAdapter { - - constructor(psv: Viewer, options: EquirectangularAdapterOptions); - -} diff --git a/types/adapters/little-planet/index.d.ts b/types/adapters/little-planet/index.d.ts deleted file mode 100644 index d3d535ae6..000000000 --- a/types/adapters/little-planet/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AbstractAdapter, Viewer } from '../..'; - -/** - * @summary Adapter for equirectangular panoramas displayed with little planet effect - */ -export class LittlePlanetAdapter extends AbstractAdapter { - - constructor(psv: Viewer); - -} diff --git a/types/buttons/AbstractButton.d.ts b/types/buttons/AbstractButton.d.ts deleted file mode 100644 index 497c3078c..000000000 --- a/types/buttons/AbstractButton.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { AbstractComponent } from '../components/AbstractComponent'; -import { Navbar } from '../components/Navbar'; - -/** - * @summary Base navbar button class - */ -export abstract class AbstractButton extends AbstractComponent { - - /** - * @summary Unique identifier of the button - */ - static id: string; - - /** - * @summary Identifier to declare a group of buttons - */ - static groupId?: string; - - /** - * @summary SVG icon name injected in the button - */ - static icon?: string; - - /** - * @summary SVG icon name injected in the button when it is active - */ - static iconActive?: string; - - constructor(navbar: Navbar, className?: string, collapsable?: boolean, tabbable?: boolean); - - /** - * @summary Checks if the button can be displayed - */ - isSupported(): boolean | { initial: boolean, promise: Promise }; - - /** - * @summary Changes the active state of the button - */ - toggleActive(active?: boolean); - - /** - * @summary Disables the button - */ - disable(); - - /** - * @summary Enables the button - */ - enable(); - - /** - * @summary Collapses the button in the navbar menu - */ - collapse(); - - /** - * @summary Uncollapses the button from the navbar menu - */ - uncollapse(); - - /** - * Action when the button is clicked - */ - abstract onClick(); - -} diff --git a/types/components/AbstractComponent.d.ts b/types/components/AbstractComponent.d.ts deleted file mode 100644 index c277d3575..000000000 --- a/types/components/AbstractComponent.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Viewer } from '../Viewer'; - -/** - * @summary Base component class - */ -export abstract class AbstractComponent { - - constructor(parent: Viewer | AbstractComponent, className?: string); - - /** - * @summary Displays the component - */ - show(options?: any); - - /** - * @summary Hides the component - */ - hide(options?: any); - - /** - * @summary Displays or hides the component - */ - toggle(visible?: boolean); - - /** - * @summary Check if the component is visible - */ - isVisible(options?: any): boolean; - -} diff --git a/types/components/Loader.d.ts b/types/components/Loader.d.ts deleted file mode 100644 index 6b2186a5b..000000000 --- a/types/components/Loader.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AbstractComponent } from './AbstractComponent'; - -/** - * @summary Loader class - */ -export class Loader extends AbstractComponent { - - /** - * @summary Sets the loader progression - * @param value - */ - setProgress(value: number); - -} diff --git a/types/components/Navbar.d.ts b/types/components/Navbar.d.ts deleted file mode 100644 index 22bf26519..000000000 --- a/types/components/Navbar.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { AbstractButton } from '../buttons/AbstractButton'; -import { NavbarCustomButton } from '../models'; -import { AbstractComponent } from './AbstractComponent'; - -/** - * @summary Register a new button available for all viewers - */ -export function registerButton(button: typeof AbstractButton, defaultPosition?: string): void; - -/** - * @summary Navigation bar class - */ -export class Navbar extends AbstractComponent { - - /** - * @summary Change the buttons visible on the navbar - */ - setButtons(buttons: string | Array); - - /** - * @summary Sets the bar caption - */ - setCaption(html: string); - - /** - * @summary Returns a button by its identifier - */ - getButton(id: string): AbstractButton; - -} diff --git a/types/components/Notification.d.ts b/types/components/Notification.d.ts deleted file mode 100644 index 99b7b29cd..000000000 --- a/types/components/Notification.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AbstractComponent } from './AbstractComponent'; - -export type NotificationOptions = { - id?: string; - content: string; - timeout?: number; -}; - -/** - * @summary Notification class - */ -export class Notification extends AbstractComponent { - - show(config: string | NotificationOptions); - -} diff --git a/types/components/Overlay.d.ts b/types/components/Overlay.d.ts deleted file mode 100644 index 3701e0d08..000000000 --- a/types/components/Overlay.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { AbstractComponent } from './AbstractComponent'; - -export type OverlayOptions = { - id?: string; - image: string; - text: string; - subtext?: string; - dissmisable?: boolean; -}; - -/** - * @summary Overlay class - */ -export class Overlay extends AbstractComponent { - - show(config: string | OverlayOptions); - - hide(id?: string); - - isVisible(id?: string): boolean; - -} diff --git a/types/components/Panel.d.ts b/types/components/Panel.d.ts deleted file mode 100644 index 42180a39a..000000000 --- a/types/components/Panel.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { AbstractComponent } from './AbstractComponent'; - -export type PanelOptions = { - id?: string; - content: string; - noMargin?: boolean; - width?: string; - clickHandler?: (e: MouseEvent) => {}; -}; - -/** - * @summary Panel class - */ -export class Panel extends AbstractComponent { - - show(config: string | PanelOptions); - - hide(id?: string); - - isVisible(id?: string): boolean; - -} diff --git a/types/components/Tooltip.d.ts b/types/components/Tooltip.d.ts deleted file mode 100644 index d2386737c..000000000 --- a/types/components/Tooltip.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { AbstractComponent } from './AbstractComponent'; - -/** - * Object defining the tooltip position - */ -export type TooltipPosition = { - top: number; - left: number; - position?: string | string[]; - box?: { width: number, height: number }; -}; - -/** - * Object defining the tooltip configuration - */ -export type TooltipOptions = TooltipPosition & { - content: string; - className?: string; - data?: any; -}; - -export class Tooltip extends AbstractComponent { - - /** - * Do not call this method directly, use {@link TooltipRenderer} instead. - */ - show(options: TooltipOptions); - - /** - * @summary Moves the tooltip to a new position - * @throws {PSVError} when the configuration is incorrect - */ - move(position: TooltipPosition); - -} diff --git a/types/data/config.d.ts b/types/data/config.d.ts deleted file mode 100644 index 4a6d644a2..000000000 --- a/types/data/config.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ViewerOptions } from '../Viewer'; - -/** - * @summary Default options - */ -export const DEFAULTS: ViewerOptions; diff --git a/types/data/constants.d.ts b/types/data/constants.d.ts deleted file mode 100644 index 4035e886d..000000000 --- a/types/data/constants.d.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @summary Radius of the THREE.SphereGeometry, Half-length of the THREE.BoxGeometry - */ -export const SPHERE_RADIUS = 10; - -/** - * @summary Property name added to viewer element - */ -export const VIEWER_DATA: 'photoSphereViewer'; - -/** - * @summary Available actions - */ -export const ACTIONS: { - ROTATE_LAT_UP: 'rotateLatitudeUp', - ROTATE_LAT_DOWN: 'rotateLatitudeDown', - ROTATE_LONG_RIGHT: 'rotateLongitudeRight', - ROTATE_LONG_LEFT: 'rotateLongitudeLeft', - ZOOM_IN: 'zoomIn', - ZOOM_OUT: 'zoomOut', - TOGGLE_AUTOROTATE: 'toggleAutorotate', -}; - -/** - * @summary Available events names - */ -export const EVENTS: { - AUTOROTATE: 'autorotate', - BEFORE_RENDER: 'before-render', - BEFORE_ROTATE: 'before-rotate', - CLICK: 'click', - CLOSE_PANEL: 'close-panel', - CONFIG_CHANGED: 'config-changed', - DOUBLE_CLICK: 'dblclick', - FULLSCREEN_UPDATED: 'fullscreen-updated', - HIDE_NOTIFICATION: 'hide-notification', - HIDE_OVERLAY: 'hide-overlay', - HIDE_TOOLTIP: 'hide-tooltip', - LOAD_PROGRESS: 'load-progress', - OPEN_PANEL: 'open-panel', - PANORAMA_LOADED: 'panorama-loaded', - POSITION_UPDATED: 'position-updated', - READY: 'ready', - RENDER: 'render', - SHOW_NOTIFICATION: 'show-notification', - SHOW_OVERLAY: 'show-overlay', - SHOW_TOOLTIP: 'show-tooltip', - SIZE_UPDATED: 'size-updated', - STOP_ALL: 'stop-all', - ZOOM_UPDATED: 'zoom-updated', -}; - -/** - * @summary Available change events names - */ -export const CHANGE_EVENTS: { - GET_ANIMATE_POSITION: 'get-animate-position', - GET_ROTATE_POSITION: 'get-rotate-position', -}; - -/** - * @summary Collection of easing functions - * @see {@link https://gist.github.com/frederickk/6165768} - */ -export const EASINGS: { - linear: (t: number) => number, - - inQuad: (t: number) => number, - outQuad: (t: number) => number, - inOutQuad: (t: number) => number, - - inCubic: (t: number) => number, - outCubic: (t: number) => number, - inOutCubic: (t: number) => number, - - inQuart: (t: number) => number, - outQuart: (t: number) => number, - inOutQuart: (t: number) => number, - - inQuint: (t: number) => number, - outQuint: (t: number) => number, - inOutQuint: (t: number) => number, - - inSine: (t: number) => number, - outSine: (t: number) => number, - inOutSine: (t: number) => number, - - inExpo: (t: number) => number, - outExpo: (t: number) => number, - inOutExpo: (t: number) => number, - - inCirc: (t: number) => number, - outCirc: (t: number) => number, - inOutCirc: (t: number) => number, -}; - -/** - * @summary Subset of key codes - */ -export const KEY_CODES: { - Enter : 'Enter', - Control : 'Control', - Escape : 'Escape', - Space : ' ', - PageUp : 'PageUp', - PageDown : 'PageDown', - ArrowLeft : 'ArrowLeft', - ArrowUp : 'ArrowUp', - ArrowRight: 'ArrowRight', - ArrowDown : 'ArrowDown', - Delete : 'Delete', - Plus : '+', - Minus : '-', -}; diff --git a/types/data/system.d.ts b/types/data/system.d.ts deleted file mode 100644 index 093f87f0a..000000000 --- a/types/data/system.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @summary General information about the system - */ -export const SYSTEM: { - loaded: boolean; - pixelRatio: number; - isWebGLSupported: boolean; - maxTextureWidth: number; - mouseWheelEvent: string; - fullscreenEvent: string; - getMaxCanvasWidth: () => number; - isTouchEnabled: Promise; -}; diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index fc22a5b35..000000000 --- a/types/index.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as CONSTANTS from './data/constants'; -import * as utils from './utils'; - -export * from './adapters/AbstractAdapter'; -export * from './adapters/equirectangular'; -export * from './buttons/AbstractButton'; -export * from './components/AbstractComponent'; -export * from './components/Loader'; -export * from './components/Navbar'; -export * from './components/Notification'; -export * from './components/Overlay'; -export * from './components/Panel'; -export * from './components/Tooltip'; -export * from './data/config'; -export * from './data/system'; -export * from './models'; -export * from './plugins/AbstractPlugin'; -export * from './PSVError'; -export * from './services/DataHelper'; -export * from './services/TextureLoader'; -export * from './services/TooltipRenderer'; -export * from './Viewer'; -export { CONSTANTS, utils }; diff --git a/types/models.d.ts b/types/models.d.ts deleted file mode 100644 index 19cde7cb2..000000000 --- a/types/models.d.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Texture } from 'three'; - -/** - * Object defining a point - */ -export type Point = { - x: number; - y: number; -} - -/** - * Object defining a size - */ -export type Size = { - width: number; - height: number; -} - -/** - * Object defining a size in CSS (px, % or auto) - */ -export type CssSize = { - width: string; - height: string; -} - -export type SphereCorrection = { - pan?: number; - tilt?: number; - roll?: number; -} - -/** - * Object defining a spherical position - */ -export type Position = { - longitude: number; - latitude: number; -} - -/** - * Object defining a spherical or texture position - */ -export type ExtendedPosition = Position | Point; - -/** - * Object defining animation options - */ -export type AnimateOptions = ExtendedPosition & { - speed: string | number; - zoom?: number; -}; - -/** - * Crop information of the panorama - */ -export type PanoData = { - fullWidth: number; - fullHeight: number; - croppedWidth: number; - croppedHeight: number; - croppedX: number; - croppedY: number; - poseHeading?: number; - posePitch?: number; - poseRoll?: number; -} - -/** - * Function to compute panorama data once the image is loaded - */ -export type PanoDataProvider = (image: HTMLImageElement) => PanoData; - -/** - * Object defining panorama and animation options - */ -export type PanoramaOptions = (ExtendedPosition | {}) & { - caption?: string; - description?: string; - transition?: boolean | number; - showLoader?: boolean; - zoom?: number; - sphereCorrection?: SphereCorrection; - panoData?: PanoData | PanoDataProvider; - overlay?: any; - overlayOpacity?: number; -}; - -/** - * Result of the AbstractAdapter#loadTexture method - */ -export type TextureData = { - panorama: any; - texture: Texture | Texture[] | Record; - panoData?: PanoData; -}; - -/** - * Data of the `click` event - */ -export type ClickData = { - rightclick: boolean; - clientX: number; - clientY: number; - viewerX: number; - viewerY: number; - longitude: number; - latitude: number; - textureX?: number; - textureY?: number; - marker?: any; -} - -/** - * Definition of a custom navbar button - */ -export type NavbarCustomButton = { - id?: string; - title?: string; - content?: string; - className?: string; - onClick: (Viewer) => void; - disabled?: boolean; - visible?: boolean; - collapsable?: boolean; -}; diff --git a/types/plugins/AbstractPlugin.d.ts b/types/plugins/AbstractPlugin.d.ts deleted file mode 100644 index 79fd81f93..000000000 --- a/types/plugins/AbstractPlugin.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { EventEmitter } from 'uevent'; -import { Viewer } from '../Viewer'; - -/** - * @summary Base plugins class - */ -export abstract class AbstractPlugin extends EventEmitter { - - /** - * @summary Unique identifier of the plugin - */ - static id: string; - - constructor(psv: Viewer); - - /** - * @summary Initializes the plugin - */ - init(); - - /** - * @summary Destroys the plugin - */ - destroy(); - -} - -export type PluginConstructor = new (psv: Viewer, options?: any) => T; diff --git a/types/plugins/autorotate-keypoints/index.d.ts b/types/plugins/autorotate-keypoints/index.d.ts deleted file mode 100644 index d43afd4e5..000000000 --- a/types/plugins/autorotate-keypoints/index.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { AbstractPlugin, ExtendedPosition, Viewer } from '../..'; - -/** - * @summary Definition of keypoints for automatic rotation, can be a position object, a marker id or an object with the following properties - */ -export type AutorotateKeypoint = string | ExtendedPosition | { - markerId?: string; - position?: ExtendedPosition; - tooltip?: string | { content: string, position: string }; - pause?: number; -}; - -export type AutorotateKeypointsPluginOptions = { - startFromClosest?: boolean; - keypoints?: AutorotateKeypoint[]; -} - -/** - * @summary Replaces the standard autorotate animation by a smooth transition between multiple points - */ -export class AutorotateKeypointsPlugin extends AbstractPlugin { - - constructor(psv: Viewer, options: AutorotateKeypointsPluginOptions); - - /** - * @summary Changes the keypoints - */ - setKeypoints(keypoints: AutorotateKeypoint[]); - -} diff --git a/types/plugins/compass/index.d.ts b/types/plugins/compass/index.d.ts deleted file mode 100644 index 76c6e06b0..000000000 --- a/types/plugins/compass/index.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { AbstractPlugin, ExtendedPosition, Viewer } from '../..'; - -export type CompassPluginOptions = { - size?: string; - position?: string; - backgroundSvg?: string; - coneColor?: string; - navigation?: boolean; - navigationColor?: string; - hotspots?: CompassPluginHotspot[]; - hotspotColor?: string; -}; - -export type CompassPluginHotspot = ExtendedPosition & { - color?: string; -}; - -/** - * @summary Adds a compass on the viewer - */ -export class CompassPlugin extends AbstractPlugin { - - constructor(psv: Viewer, options: CompassPluginOptions); - - /** - * @summary Hides the compass - */ - hide(); - - /** - * @summary Shows the compass - */ - show(); - - /** - * @summary Changes the hotspots on the compass - */ - setHotspots(hotspots); - - /** - * @summary Removes all hotspots - */ - clearHotspots(); - -} diff --git a/types/plugins/gallery/index.d.ts b/types/plugins/gallery/index.d.ts deleted file mode 100644 index 7d70bc1ce..000000000 --- a/types/plugins/gallery/index.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { AbstractPlugin, PanoramaOptions, Size, Viewer } from '../..'; - -export type GalleryPluginOptions = { - items?: GalleryPluginItem[]; - visibleOnLoad?: boolean; - hideOnClick?: boolean; - thumbnailSize?: Size; -}; - -export type GalleryPluginItem = { - id: number | string; - panorama: any; - thumbnail?: string; - name?: string; - options?: PanoramaOptions; -}; - -export const EVENTS: { - SHOW_GALLERY: 'show-gallery', - HIDE_GALLERY: 'hide-gallery', -}; - -/** - * @summary Adds a gallery of multiple panoramas - */ -export class GalleryPlugin extends AbstractPlugin { - - static EVENTS: typeof EVENTS; - - constructor(psv: Viewer, options: GalleryPluginOptions); - - /** - * @summary Hides the gallery - */ - hide(); - - /** - * @summary Shows the gallery - */ - show(); - - /** - * @summary Hides or shows the gallery - */ - toggle(); - - /** - * @summary Sets the list of items - */ - setItems(items?: GalleryPluginItem[]); - - /** - * @summary Triggered when the gallery is shown - */ - on(e: 'show-gallery'): this; - - /** - * @summary Triggered when the gallery is hidden - */ - on(e: 'hide-gallery'): this; - -} diff --git a/types/plugins/gyroscope/index.d.ts b/types/plugins/gyroscope/index.d.ts deleted file mode 100644 index 73d9b931b..000000000 --- a/types/plugins/gyroscope/index.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { AbstractPlugin, Viewer } from '../..'; -import { Event } from 'uevent'; - -export type GyroscopePluginOptions = { - touchmove?: boolean; - absolutePosition?: boolean; - moveMode?:'smooth' | 'fast' -}; - -export const EVENTS: { - GYROSCOPE_UPDATED: 'gyroscope-updated', -}; - -/** - * @summary Adds gyroscope controls on mobile devices - */ -export class GyroscopePlugin extends AbstractPlugin { - - static EVENTS: typeof EVENTS; - - constructor(psv: Viewer, options: GyroscopePluginOptions); - - /** - * @summary Checks if the gyroscope is enabled - */ - isEnabled(): boolean; - - /** - * @summary Enables the gyroscope navigation if available - * @throws {PSVError} if the gyroscope API is not available/granted - */ - start(): Promise; - - /** - * @summary Disables the gyroscope navigation - */ - stop(); - - /** - * @summary Enables or disables the gyroscope navigation - */ - toggle(); - - /** - * @summary Triggered when the gyroscope mode is enabled/disabled - */ - on(e: 'gyroscope-updated', cb: (e: Event, enabled: boolean) => void): this; - -} diff --git a/types/plugins/markers/index.d.ts b/types/plugins/markers/index.d.ts deleted file mode 100644 index e0b28005d..000000000 --- a/types/plugins/markers/index.d.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { AbstractPlugin, ExtendedPosition, utils, Viewer } from '../..'; -import { Event } from 'uevent'; - -export type MarkerType = - 'image' - | 'imageLayer' - | 'html' - | 'square' - | 'rect' - | 'circle' - | 'ellipse' - | 'path' - | 'polygonPx' - | 'polygonRad' - | 'polylinePx' - | 'polylineRad'; - -/** - * @summary Marker properties - */ -export type MarkerProperties = Partial & { - image?: string; - imageLayer?: string; - html?: string; - square?: number; - rect?: [number, number] | { width: number, height: number }; - circle?: number; - ellipse?: [number, number] | { cx: number, cy: number }; - path?: string; - polygonPx?: [number, number][]; - polygonRad?: [number, number][]; - polylinePx?: [number, number][]; - polylineRad?: [number, number][]; - - id: string; - width?: number; - height?: number; - orientation?: 'front' | 'horizontal' | 'vertical-left' | 'vertical-right'; - scale?: [number, number] | { zoom?: [number, number], longitude?: [number, number] }; - opacity?: number; - className?: string; - style?: Record; - svgStyle?: Record; - anchor?: string; - zoomLvl?: number; - visible?: boolean; - tooltip?: string | { content: string, position?: string, className?: string, trigger?: 'hover' | 'click' }; - content?: string; - listContent?: string; - hideList?: boolean; - data?: any; -}; - -/** - * @summary Data of the `select-marker` event - */ -export type SelectMarkerData = { - dblclick: boolean; - rightclick: boolean; -}; - -export type MarkersPluginOptions = { - clickEventOnMarker?: boolean; - gotoMarkerSpeed?: string | number; - markers?: MarkerProperties[]; -}; - -/** - * @summary Object representing a marker - */ -export class Marker { - - private constructor(); - - readonly id: string; - readonly type: MarkerType; - readonly visible: boolean; - readonly config: MarkerProperties; - readonly data?: any; - - /** - * @summary Checks if it is a 3D marker (imageLayer) - */ - is3d(): boolean; - - /** - * @summary Checks if it is a normal marker (image or html) - */ - isNormal(): boolean; - - /** - * @summary Checks if it is a polygon/polyline marker - */ - isPoly(): boolean; - - /** - * @summary Checks if it is a polygon/polyline using pixel coordinates - */ - isPolyPx(): boolean; - - /** - * @summary Checks if it is a polygon/polyline using radian coordinates - */ - isPolyRad(): boolean; - - /** - * @summary Checks if it is a polygon marker - */ - isPolygon(): boolean; - - /** - * @summary Checks if it is a polyline marker - */ - isPolyline(): boolean; - - /** - * @summary Checks if it is an SVG marker - */ - isSvg(): boolean; - -} - -export const EVENTS: { - MARKER_VISIBILITY : 'marker-visibility', - GOTO_MARKER_DONE: 'goto-marker-done', - LEAVE_MARKER: 'leave-marker', - OVER_MARKER: 'over-marker', - RENDER_MARKERS_LIST: 'render-markers-list', - SELECT_MARKER: 'select-marker', - SELECT_MARKER_LIST: 'select-marker-list', - UNSELECT_MARKER: 'unselect-marker', - HIDE_MARKERS: 'hide-markers', - SET_MARKERS: 'set-markers', - SHOW_MARKERS: 'show-markers', -}; - -/** - * @summary Displays various markers on the viewer - */ -export class MarkersPlugin extends AbstractPlugin { - - static EVENTS: typeof EVENTS; - - constructor(psv: Viewer, options: MarkersPluginOptions); - - /** - * @summary Toggles the visibility of all tooltips - */ - toggleAllTooltips(); - - /** - * @summary Displays all tooltips - */ - showAllTooltips(); - - /** - * @summary Hides all tooltips - */ - hideAllTooltips(); - - /** - * @summary Returns the total number of markers - * @returns {number} - */ - getNbMarkers(): number; - - /** - * @summary Returns all the markers - */ - getMarkers(): Marker[]; - - /** - * @summary Adds a new marker to viewer - * @throws {PSVError} when the marker's id is missing or already exists - */ - addMarker(properties: MarkerProperties, render?: boolean): Marker; - - /** - * @summary Returns the internal marker object for a marker id - * @throws {PSVError} when the marker cannot be found - */ - getMarker(markerId: string): Marker; - - /** - * @summary Returns the last marker selected by the user - */ - getCurrentMarker(): Marker; - - /** - * @summary Updates the existing marker with the same id - * @description Every property can be changed but you can't change its type (Eg: `image` to `html`). - */ - updateMarker(properties: MarkerProperties, render?: boolean): Marker; - - /** - * @summary Removes a marker from the viewer - */ - removeMarker(markerId: string, render?: boolean); - - /** - * @summary Removes multiple markers - */ - removeMarkers(markerIds, render?: boolean); - - /** - * @summary Replaces all markers - */ - setMarkers(markers: MarkerProperties[], render?: boolean); - - /** - * @summary Removes all markers - */ - clearMarkers(render?: boolean); - - /** - * @summary Rotate the view to face the marker - */ - gotoMarker(markerId: string, speed: string | number): utils.Animation; - - /** - * @summary Hides a marker - */ - hideMarker(markerId: string); - - /** - * @summary Shows a marker - */ - showMarker(markerId: string); - - /** - * @summary Toggles a marker - */ - toggleMarker(markerId: string); - - /** - * @summary Forces the display of the tooltip - */ - showMarkerTooltip(markerId: string); - - /** - * @summary Hides the tooltip - */ - hideMarkerTooltip(markerId: string); - - /** - * @summary Opens the panel with the content of the marker - */ - showMarkerPanel(markerId: string); - - /** - * @summary Toggles the visibility of the list of markers - */ - toggleMarkersList(); - - /** - * @summary Opens side panel with the list of markers - */ - showMarkersList(); - - /** - * @summary Closes side panel if it contains the list of markers - */ - hideMarkersList(); - - /** - * @summary Updates the visibility and the position of all markers - */ - renderMarkers(); - - /** - * @summary Triggered when the visibility of a marker changes - */ - on(e: 'marker-visibility', cb: (e: Event, marker: Marker, visible: boolean) => void): this; - - /** - * @summary Triggered when the animation to a marker is done - */ - on(e: 'goto-marker-done', cb: (e: Event, marker: Marker) => void): this; - - /** - * @summary Triggered when the user puts the cursor away from a marker - */ - on(e: 'leave-marker', cb: (e: Event, marker: Marker) => void): this; - - /** - * @summary Triggered when the user puts the cursor hover a marker - */ - on(e: 'over-marker', cb: (e: Event, marker: Marker) => void): this; - - /** - * @summary Used to alter the list of markers displayed on the side-panel - */ - on(e: 'render-markers-list', cb: (e: Event, markers: Marker[]) => Marker[]): this; - - /** - * @summary Triggered when the user clicks on a marker. The marker can be retrieved from outside the event handler - * with {@link MarkersPlugin.getCurrentMarker} - */ - on(e: 'select-marker', cb: (e: Event, marker: Marker, data: SelectMarkerData) => void): this; - - /** - * @summary Triggered when a marker is selected from the side panel - */ - on(e: 'select-marker-list', cb: (e: Event, marker: Marker) => void): this; - - /** - * @summary Triggered when a marker was selected and the user clicks elsewhere - */ - on(e: 'unselect-marker', cb: (e: Event, marker: Marker) => void): this; - - /** - * @summary Triggered when the markers are hidden - */ - on(e: 'hide-markers', cb: (e: Event) => void): this; - - /** - * @summary Triggered when the markers are shown - */ - on(e: 'show-markers', cb: (e: Event) => void): this; - -} diff --git a/types/plugins/resolution/index.d.ts b/types/plugins/resolution/index.d.ts deleted file mode 100644 index 6247100cd..000000000 --- a/types/plugins/resolution/index.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AbstractPlugin, Viewer } from '../..'; -import { Event } from 'uevent'; - -export type Resolution = { - id: string; - label: string; - panorama: any; -}; - -export type ResolutionPluginOptions = { - resolutions: Resolution[]; - defaultResolution?: string; - showBadge?: boolean; -}; - -export const EVENTS: { - RESOLUTION_CHANGED: 'resolution-changed', -}; - -/** - * @summary Adds a setting to choose between multiple resolutions of the panorama. - */ -export class ResolutionPlugin extends AbstractPlugin { - - static EVENTS: typeof EVENTS; - - constructor(psv: Viewer, options: ResolutionPluginOptions); - - /** - * @summary Changes the available resolutions - */ - setResolutions(resolutions: Resolution[], defaultResolution?: string); - - /** - * @summary Changes the current resolution - * @throws {PSVError} if the resolution does not exist - */ - setResolution(id: string); - - /** - * @summary Returns the current resolution - */ - getResolution(): string; - - /** - * @summary Triggered when the resolution is changed - */ - on(e: 'resolution-changed', cb: (e: Event, resolutionId: string) => void): this; - -} diff --git a/types/plugins/settings/index.d.ts b/types/plugins/settings/index.d.ts deleted file mode 100644 index 5798c1c94..000000000 --- a/types/plugins/settings/index.d.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Event } from 'uevent'; -import { AbstractPlugin, Viewer } from '../..'; - -/** - * @summary Description of a setting - */ -export type BaseSetting = { - id: string; - label: string; - badge?: () => string; -}; - -/** - * @summary Description of a 'options' setting - */ -export type OptionsSetting = BaseSetting & { - type: 'options'; - current: () => string; - options: () => SettingOption[] - apply: (string) => void; -}; - -/** - * @summary Description of a 'toggle' setting - */ -export type ToggleSetting = BaseSetting & { - type: 'toggle'; - active: () => boolean; - toggle: () => void; -}; - -/** - * @summary Option of an 'option' setting - */ -export type SettingOption = { - id: string; - label: string; -}; - -export type Setting = OptionsSetting | ToggleSetting; - -export type SettingsPluginOptions = { - persist?: boolean; - storage?: { - get(settingId: string): boolean | string | Promise; - set(settingId: string, value: boolean | string); - }; -}; - -export const EVENTS: { - SETTING_CHANGED: 'setting-changed', -}; - -export const TYPE_OPTIONS = 'options'; -export const TYPE_TOGGLE = 'toggle'; - -/** - * @summary Adds a button to access various settings. - */ -export class SettingsPlugin extends AbstractPlugin { - - static EVENTS: typeof EVENTS; - static TYPE_OPTIONS: typeof TYPE_OPTIONS; - static TYPE_TOGGLE: typeof TYPE_TOGGLE; - - constructor(psv: Viewer); - - /** - * @summary Registers a new setting - */ - addSetting(setting: Setting); - - /** - * @summary Removes a setting - * @param {string} id - */ - removeSetting(id: string); - - /** - * @summary Toggles the settings panel - */ - toggleSettings(); - - /** - * @summary Hides the settings panel - */ - hideSettings(); - - /** - * @summary Shows the settings panel - */ - showSettings(); - - /** - * @summary Triggered when a setting is changed - */ - on(e: 'setting-changed', cb: (e: Event, settingId: string, value: any) => void): this; - -} diff --git a/types/plugins/stereo/index.d.ts b/types/plugins/stereo/index.d.ts deleted file mode 100644 index 447f1f045..000000000 --- a/types/plugins/stereo/index.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { AbstractPlugin, Viewer } from '../..'; -import { Event } from 'uevent'; - -export const EVENTS: { - STEREO_UPDATED: 'stereo-updated', -}; - -/** - * @summary Adds stereo view on mobile devices - */ -export class StereoPlugin extends AbstractPlugin { - - static EVENTS: typeof EVENTS; - - constructor(psv: Viewer); - - /** - * @summary Checks if the stereo view is enabled - */ - isEnabled(): boolean; - - /** - * @summary Enables the stereo view - * @throws {PSVError} if the gyroscope API is not available/granted - */ - start(): Promise; - - /** - * @summary Disables the stereo view - */ - stop(); - - /** - * @summary Enables or disables the stereo view - */ - toggle(); - - /** - * @summary Triggered when the stereo view is enabled/disabled - */ - on(e: 'stereo-updated', cb: (e: Event, enabled: boolean) => void): this; - -} diff --git a/types/plugins/video/index.d.ts b/types/plugins/video/index.d.ts deleted file mode 100644 index 13705f995..000000000 --- a/types/plugins/video/index.d.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Event } from 'uevent'; -import { AbstractPlugin, ExtendedPosition, Viewer } from '../..'; - -export const EVENTS: { - PLAY: 'play', - PAUSE: 'pause', - VOLUME_CHANGE: 'volume-change', - PROGRESS: 'progress', - BUFFER: 'buffer', -}; - -/** - * @summary Definition of keypoints for automatic rotation, can be a position object, a marker id or an object with the following properties - */ -export type AutorotateKeypoint = { - position: ExtendedPosition; - time: number; -}; - -export type VideoPluginOptions = { - progressbar?: boolean; - bigbutton?: boolean; - keypoints?: AutorotateKeypoint[]; -}; - -/** - * @summary Controls a video adapter - */ -export class VideoPlugin extends AbstractPlugin { - - static EVENTS: typeof EVENTS; - - constructor(psv: Viewer, options: VideoPluginOptions); - - /** - * @summary Changes the keypoints - */ - setKeypoints(keypoints: AutorotateKeypoint[]); - - /** - * @summary Returns the durection of the video - */ - getDuration(): number; - - /** - * @summary Returns the current time of the video - */ - getTime(): number; - - /** - * @summary Returns the play progression of the video - */ - getProgress(): number; - - /** - * @summary Returns if the video is playing - */ - isPlaying(): boolean; - - /** - * @summary Returns the video volume - */ - getVolume(): number; - - /** - * @summary Starts or pause the video - */ - playPause(): void; - - /** - * @summary Starts the video if paused - */ - play(): void; - - /** - * @summary Pauses the cideo if playing - */ - pause(): void; - - /** - * @summary Sets the volume of the video - */ - setVolume(volume: number): void; - - /** - * @summary (Un)mutes the video - */ - setMute(mute?: boolean): void; - - /** - * @summary Changes the current time of the video - */ - setTime(time: number): void; - - /** - * @summary Changes the progression of the video - */ - setProgress(progress: number): void; - - /** - * @summary Triggered when the video starts playing - */ - on(e: 'play', cb: (e: Event) => void): this; - - /** - * @summary Triggered when the video is paused - */ - on(e: 'pause', cb: (e: Event) => void): this; - - /** - * @summary Triggered when the video volume changes - */ - on(e: 'volume-change', cb: (e: Event, volume: number) => void): this; - - /** - * @summary Triggered when the video play progression changes - */ - on(e: 'progress', cb: (e: Event, data: { time: number, duration: number, progress: number }) => void): this; - - /** - * @summary Triggered when the video buffer changes - */ - on(e: 'buffer', cb: (e: Event, maxBuffer: number) => void): this; - -} diff --git a/types/plugins/virtual-tour/index.d.ts b/types/plugins/virtual-tour/index.d.ts deleted file mode 100644 index c761ca48f..000000000 --- a/types/plugins/virtual-tour/index.d.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Event } from 'uevent'; -import { AbstractPlugin, Position, Viewer, ViewerOptions } from '../..'; -import { MarkerProperties } from '../markers'; - -/** - * @summary Definition of a single node in the tour - */ -export type VirtualTourNode = { - id: string; - panorama: any; - links?: VirtualTourNodeLink[]; - position?: [number, number, number?]; - panoData?: ViewerOptions['panoData']; - sphereCorrection?: ViewerOptions['sphereCorrection']; - name?: string; - caption?: string; - description?: string; - markers?: MarkerProperties[]; -}; - -/** - * @summary Definition of a link between two nodes - */ -export type VirtualTourNodeLink = { - nodeId: string; - name?: string; - position?: [number, number, number?]; - markerStyle?: VirtualTourMarkerStyle; - arrowStyle?: VirtualTourArrowStyle; -}; - -/** - * @summary Style of the arrow in 3D mode - */ -export type VirtualTourArrowStyle = { - color?: string; - hoverColor?: string; - outlineColor?: number; - scale?: [number, number]; -}; - -/** - * @summary Style of the marker in markers mode - */ -export type VirtualTourMarkerStyle = Omit; - -/** - * @summary Data associated to the "node-changed" event - */ -export type VirtualTourNodeChangedData = { - fromNode?: VirtualTourNode, - fromLink?: VirtualTourNodeLink, - fromLinkPosition?: Position, -}; - -export type VirtualTourPluginOptions = { - dataMode?: 'client' | 'server'; - positionMode?: 'manual' | 'gps'; - renderMode?: '3d' | 'markers'; - nodes?: VirtualTourNode[]; - getNode?: (nodeId: string) => VirtualTourNode | Promise; - startNodeId?: string; - preload?: boolean | ((node: VirtualTourNode, link: VirtualTourNodeLink) => boolean); - rotateSpeed?: boolean | string | number; - transition?: boolean | number; - markerStyle?: VirtualTourMarkerStyle; - arrowStyle?: VirtualTourArrowStyle; - markerLatOffset?: number; - arrowPosition?: 'top' | 'bottom'; -}; - -/** - * @deprecated Use VirtualTourPluginOptions - */ -export type VirtualTourPluginPluginOptions = VirtualTourPluginOptions; - -export const EVENTS: { - NODE_CHANGED: 'node-changed', -}; - -export const MODE_CLIENT = 'client'; -export const MODE_SERVER = 'server'; -export const MODE_MANUAL = 'manual'; -export const MODE_GPS = 'gps'; -export const MODE_MARKERS = 'markers'; -export const MODE_3D = '3d'; - -/** - * @summary Create virtual tours by linking multiple panoramas - */ -export class VirtualTourPlugin extends AbstractPlugin { - - static EVENTS: typeof EVENTS; - static MODE_CLIENT: typeof MODE_CLIENT; - static MODE_SERVER: typeof MODE_SERVER; - static MODE_3D: typeof MODE_3D; - static MODE_MARKERS: typeof MODE_MARKERS; - static MODE_MANUAL: typeof MODE_MANUAL; - static MODE_GPS: typeof MODE_GPS; - - constructor(psv: Viewer, options: VirtualTourPluginOptions); - - /** - * @summary Sets the nodes (client mode only) - */ - setNodes(nodes: VirtualTourNode[], startNodeId?: string); - - /** - * @summary Changes the current node - * @returns resolves false if the loading was aborted by another call - */ - setCurrentNode(nodeId: string): Promise; - - /** - * @summary Triggered when the current node changes - */ - on(e: 'node-changed', cb: (e: Event, nodeId: VirtualTourNode['id'], data: VirtualTourNodeChangedData) => void): this; - - /** - * @summary Used to alter the list of nodes displayed on the side-panel - */ - on(e: 'render-nodes-list', cb: (e: Event, nodes: VirtualTourNode[]) => VirtualTourNode[]): this; - -} diff --git a/types/plugins/visible-range/index.d.ts b/types/plugins/visible-range/index.d.ts deleted file mode 100644 index 3b3001690..000000000 --- a/types/plugins/visible-range/index.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { AbstractPlugin, Viewer } from '../..'; - -export type VisibleRangePluginOptions = { - latitudeRange?: number[] | string[]; - longitudeRange?: number[] | string[]; - usePanoData: boolean; -}; - -/** - * @summary Locks visible longitude and/or latitude - */ -export class VisibleRangePlugin extends AbstractPlugin { - - constructor(psv: Viewer, options: VisibleRangePluginOptions); - - /** - * @summary Changes the latitude range - */ - setLatitudeRange(range: number[] | string[]); - - /** - * @summary Changes the longitude range - */ - setLongitudeRange(range: number[] | string[]); - - /** - * @summary Changes the latitude and longitude ranges according the current panorama cropping data - */ - setRangesFromPanoData() - -} diff --git a/types/services/DataHelper.d.ts b/types/services/DataHelper.d.ts deleted file mode 100644 index 2a1981b10..000000000 --- a/types/services/DataHelper.d.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Vector3, Intersection } from 'three'; -import { ExtendedPosition, PanoData, Point, Position, SphereCorrection } from '../models'; - -/** - * @summary Collections of data converters for the current viewer - */ -export class DataHelper { - - /** - * @summary Converts vertical FOV to zoom level - */ - fovToZoomLevel(fov: number): number; - - /** - * @summary Converts zoom level to vertical FOV - */ - zoomLevelToFov(level: number): number; - - /** - * @summary Convert vertical FOV to horizontal FOV - */ - vFovToHFov(vFov: number): number; - - /** - * @summary Converts a speed into a duration from current position to a new position - */ - speedToDuration(value: string | number, angle: number): number; - - /** - * @summary Converts pixel texture coordinates to spherical radians coordinates - */ - textureCoordsToSphericalCoords(point: Point): Position; - - /** - * @summary Converts spherical radians coordinates to pixel texture coordinates - */ - sphericalCoordsToTextureCoords(position: Position): Point; - - /** - * @summary Converts spherical radians coordinates to a THREE.Vector3 - */ - sphericalCoordsToVector3(position: Position, vector: Vector3): Vector3; - - /** - * @summary Converts a THREE.Vector3 to spherical radians coordinates - */ - vector3ToSphericalCoords(vector: Vector3): Position; - - /** - * @summary Converts position on the viewer to a THREE.Vector3 - */ - viewerCoordsToVector3(point: Point): Vector3; - - /** - * @summary Converts a THREE.Vector3 to position on the viewer - */ - vector3ToViewerCoords(vector: Vector3): Point; - - /** - * @summary Converts spherical radians coordinates to position on the viewer - */ - sphericalCoordsToViewerCoords(position: Position): Point; - - /** - * @summary Returns intersections with objects in the scene - */ - getIntersections(viewerPoint: Point): Intersection[]; - - /** - * @summary Converts x/y to latitude/longitude if present and ensure boundaries - */ - cleanPosition(position: ExtendedPosition): Position; - - /** - * @summary Ensure a SphereCorrection object is valid - */ - cleanSphereCorrection(sphere: SphereCorrection): SphereCorrection; - - /** - * @summary Parse the pose angles of the pano data - */ - cleanPanoramaPose(panoData: PanoData): SphereCorrection; - -} diff --git a/types/services/TextureLoader.d.ts b/types/services/TextureLoader.d.ts deleted file mode 100644 index ffbe00385..000000000 --- a/types/services/TextureLoader.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @summary Texture loader - */ -export class TextureLoader { - - /** - * @summary Cancels current HTTP requests - */ - abortLoading(); - - /** - * @summary Loads a Blob with FileLoader - */ - loadFile(url: string, onProgress?: (number) => void): Promise; - - /** - * @summary Loads an Image using FileLoader to have progress events - */ - loadImage(url: string, onProgress?: (number) => void): Promise; - - /** - * @summary Preload a panorama file without displaying it - */ - preloadPanorama(panorama: any): Promise; - -} diff --git a/types/services/TooltipRenderer.d.ts b/types/services/TooltipRenderer.d.ts deleted file mode 100644 index 00587c7a9..000000000 --- a/types/services/TooltipRenderer.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Tooltip, TooltipOptions } from '../components/Tooltip'; -import { AbstractComponent } from '../components/AbstractComponent'; - -/** - * @summary Tooltip renderer - */ -export class TooltipRenderer extends AbstractComponent { - - /** - * @summary Displays a tooltip on the viewer - * @throws {PSVError} when the configuration is incorrect - */ - create(config: TooltipOptions): Tooltip; - -} diff --git a/types/utils/Animation.d.ts b/types/utils/Animation.d.ts deleted file mode 100644 index 681fc3cbc..000000000 --- a/types/utils/Animation.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -export type AnimationOptions = { - properties: { [key in keyof T]: { start: number, end: number } }; - duration: number; - delay?: number; - easing?: string | ((progress: number) => number); - onTick: (properties: { [key in keyof T]: number }, progress: number) => void; -}; - -/** - * @summary Interpolation helper for animations - * @description - * Implements the Promise API with an additional "cancel" method. - * The promise is resolved when the animation is complete and rejected if the animation is cancelled. - */ -export class Animation implements PromiseLike { - - constructor(options: AnimationOptions); - - then(onFulfilled?: ((completed: boolean) => TResult | PromiseLike) | undefined | null): PromiseLike; - - cancel(); - -} diff --git a/types/utils/browser.d.ts b/types/utils/browser.d.ts deleted file mode 100644 index 6e4d3b729..000000000 --- a/types/utils/browser.d.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @summary Toggles a CSS class - */ -export function toggleClass(element: HTMLElement | SVGElement, className: string, active?: boolean); - -/** - * @summary Adds one or several CSS classes to an element - */ -export function addClasses(element: HTMLElement, className: string); - -/** - * @summary Removes one or several CSS classes to an element - */ -export function removeClasses(element: HTMLElement, className: string); - -/** - * @summary Searches if an element has a particular parent at any level including itself - */ -export function hasParent(el: HTMLElement, parent: HTMLElement): boolean; - -/** - * @summary Gets the closest parent (can by itself) - */ -export function getClosest(el: HTMLElement | SVGElement, selector: string): HTMLElement; - -/** - * @summary Detects if fullscreen is enabled - */ -export function isFullscreenEnabled(elt: HTMLElement): boolean; - -/** - * @summary Enters fullscreen mode - */ -export function requestFullscreen(elt: HTMLElement); - -/** - * @summary Exits fullscreen mode - */ -export function exitFullscreen(); - -/** - * @summary Gets an element style - */ -export function getStyle(elt: HTMLElement, prop: string): any; - -/** - * @summary Normalize mousewheel values accross browsers - * @description From Facebook's Fixed Data Table - * {@link https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js} - * @copyright Facebook - */ -export function normalizeWheel(event: WheelEvent): { spinX: number, spinY: number, pixelX: number, pixelY: number }; diff --git a/types/utils/index.d.ts b/types/utils/index.d.ts deleted file mode 100644 index 5fa8d1bf1..000000000 --- a/types/utils/index.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './browser'; -export * from './math'; -export * from './misc'; -export * from './psv'; - -export * from './Animation'; diff --git a/types/utils/math.d.ts b/types/utils/math.d.ts deleted file mode 100644 index de9db6ff8..000000000 --- a/types/utils/math.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Point, Position } from '../models'; - -/** - * @summary Ensures that a number is in a given interval - */ -export function bound(x: number, min: number, max: number): number; - -/** - * @summary Checks if a value is an integer - */ -export function isInteger(value: any): boolean; - -/** - * @summary Computes the sum of an array - */ -export function sum(array: number[]): number; - -/** - * @summary Computes the distance between two points - */ -export function distance(p1: Point, p2: Point): number; - -/** - * @summary Compute the shortest offset between two longitudes - */ -export function getShortestArc(from: number, to: number): number; - -/** - * @summary Computes the angle between the current position and a target position - */ -export function getAngle(position1: Position, position2: Position): number; - -/** - * @summary Returns the distance between two points on a sphere of radius one - */ -export function greatArcDistance(p1: number[], p2: number[]): number; diff --git a/types/utils/misc.d.ts b/types/utils/misc.d.ts deleted file mode 100644 index feb96cfc8..000000000 --- a/types/utils/misc.d.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @summary Transforms a string to dash-case {@link https://github.com/shahata/dasherize} - */ - -export function dasherize(str: string): string; - -/** - * @summary Returns a function, that, when invoked, will only be triggered at most once during a given window of time. - * @copyright underscore.js - modified by Clément Prévost {@link http://stackoverflow.com/a/27078401} - */ -export function throttle(func: Function, wait: number): Function; - -/** - * @summary Test if an object is a plain object - * @description Test if an object is a plain object, i.e. is constructed - * by the built-in Object constructor and inherits directly from Object.prototype - * or null. Some built-in objects pass the test, e.g. Math which is a plain object - * and some host or exotic objects may pass also. - * {@link http://stackoverflow.com/a/5878101/1207670} - */ -export function isPlainObject(obj: any): boolean; - -/** - * @summary Merges the enumerable attributes of two objects - * @description Replaces arrays and alters the target object. - * @copyright Nicholas Fisher - */ -export function deepmerge(target: object, src: object): object; - -/** - * @summary Deeply clones an object - */ -export function clone(src: object): object; - -/** - * @summery Test of an object is empty - */ -export function isEmpty(obj: object): boolean; - -/** - * @summary Loops over enumerable properties of an object - */ -export function each(object: object, callback: (value: any, key: string) => void); - -/** - * @summary Returns the intersection between two arrays - */ -export function intersect(array1: T[], array2: T[]): T[]; - -/** - * @summary Returns if a valu is null or undefined - */ -export function isNil(val: any): val is null | undefined; - -/** - * @summary Returns the first non null non undefined parameter - */ -export function firstNonNull(...values: any[]): any; diff --git a/types/utils/psv.d.ts b/types/utils/psv.d.ts deleted file mode 100644 index f960f0b7c..000000000 --- a/types/utils/psv.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Euler, Texture, Vector3 } from 'three'; -import { ExtendedPosition, Point } from '../models'; - -/** - * @summary Displays a warning in the console - */ -export function logWarn(message: string); - -/** - * @summary Checks if an object is a {PSV.ExtendedPosition}, ie has x/y or longitude/latitude - */ -export function isExtendedPosition(object: any): object is ExtendedPosition; - -/** - * @summary Returns the value of a given attribute in the panorama metadata - */ -export function getXMPValue(data: string, attr: string): number | null; - -/** - * @summary Translate CSS values like "top center" or "10% 50%" as top and left positions - * @description The implementation is as close as possible to the "background-position" specification - * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/background-position} - */ -export function parsePosition(value: string | Point): Point; - -/** - * @summary Parse a CSS-like position into an array of position keywords among top, bottom, left, right and center - */ -export function cleanPosition(value: string | string[], options?: { allowCenter: boolean, cssOrder: boolean }): string[]; - -/** - * @summary Parses an speed - * @param speed - The speed, in radians/degrees/revolutions per second/minute - * @returns radians per second - * @throws {PSVError} when the speed cannot be parsed - */ -export function parseSpeed(speed: string | number): number; - -/** - * @summary Parses an angle value in radians or degrees and returns a normalized value in radians - * @param {string|number} angle - eg: 3.14, 3.14rad, 180deg - * @param {boolean} [zeroCenter=false] - normalize between -Pi - Pi instead of 0 - 2*Pi - * @param {boolean} [halfCircle=zeroCenter] - normalize between -Pi/2 - Pi/2 instead of -Pi - Pi - * @throws {PSVError} when the angle cannot be parsed - */ -export function parseAngle(angle: string | number, zeroCenter?: boolean, halfCircle?: boolean): number; - -/** - * @summary Creates a THREE texture from an image - */ -export function createTexture(img: HTMLImageElement | HTMLCanvasElement): Texture; - -/** - * @summary Applies the inverse of Euler angles to a vector - */ -export function applyEulerInverse(vector: Vector3, euler: Euler); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000..66d145f83 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,11118 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.0", "@babel/compat-data@^7.20.1": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.1.tgz#f2e6ef7790d8c8dbf03d379502dcc246dcce0b30" + integrity sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ== + +"@babel/core@^7.11.0", "@babel/core@^7.8.4": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.2.tgz#8dc9b1620a673f92d3624bd926dc49a52cf25b92" + integrity sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.2" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-module-transforms" "^7.20.2" + "@babel/helpers" "^7.20.1" + "@babel/parser" "^7.20.2" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.1" + "@babel/types" "^7.20.2" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/generator@^7.20.1", "@babel/generator@^7.20.2": + version "7.20.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.4.tgz#4d9f8f0c30be75fd90a0562099a26e5839602ab8" + integrity sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA== + dependencies: + "@babel/types" "^7.20.2" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" + integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" + integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.18.6" + "@babel/types" "^7.18.9" + +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.9.6": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz#6bf5374d424e1b3922822f1d9bdaa43b1a139d0a" + integrity sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ== + dependencies: + "@babel/compat-data" "^7.20.0" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.21.3" + semver "^6.3.0" + +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.2.tgz#3c08a5b5417c7f07b5cf3dfb6dc79cbec682e8c2" + integrity sha512-k22GoYRAHPYr9I+Gvy2ZQlAe5mGy8BqWst2wRt8cwIufWTxrsVshhIBvYNqC80N0GSFWTsqRVexOtfzlgOEDvA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-member-expression-to-functions" "^7.18.9" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-replace-supers" "^7.19.1" + "@babel/helper-split-export-declaration" "^7.18.6" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz#7976aca61c0984202baca73d84e2337a5424a41b" + integrity sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + regexpu-core "^5.1.0" + +"@babel/helper-define-polyfill-provider@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a" + integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== + dependencies: + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-plugin-utils" "^7.16.7" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + +"@babel/helper-explode-assignable-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" + integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" + integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== + dependencies: + "@babel/template" "^7.18.10" + "@babel/types" "^7.19.0" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-member-expression-to-functions@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815" + integrity sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg== + dependencies: + "@babel/types" "^7.18.9" + +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.18.6", "@babel/helper-module-imports@^7.8.3": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.19.6", "@babel/helper-module-transforms@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712" + integrity sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.1" + "@babel/types" "^7.20.2" + +"@babel/helper-optimise-call-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" + integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" + integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== + +"@babel/helper-remap-async-to-generator@^7.18.6", "@babel/helper-remap-async-to-generator@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" + integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-wrap-function" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz#e1592a9b4b368aa6bdb8784a711e0bcbf0612b78" + integrity sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-member-expression-to-functions" "^7.18.9" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/traverse" "^7.19.1" + "@babel/types" "^7.19.0" + +"@babel/helper-simple-access@^7.19.4", "@babel/helper-simple-access@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" + integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== + dependencies: + "@babel/types" "^7.20.2" + +"@babel/helper-skip-transparent-expression-wrappers@^7.18.9": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" + integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== + dependencies: + "@babel/types" "^7.20.0" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== + +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + +"@babel/helper-wrap-function@^7.18.9": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz#89f18335cff1152373222f76a4b37799636ae8b1" + integrity sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg== + dependencies: + "@babel/helper-function-name" "^7.19.0" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.19.0" + "@babel/types" "^7.19.0" + +"@babel/helpers@^7.20.1": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.1.tgz#2ab7a0fcb0a03b5bf76629196ed63c2d7311f4c9" + integrity sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg== + dependencies: + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.1" + "@babel/types" "^7.20.0" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.18.10", "@babel/parser@^7.18.4", "@babel/parser@^7.20.1", "@babel/parser@^7.20.2": + version "7.20.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2" + integrity sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" + integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz#a11af19aa373d68d561f08e0a57242350ed0ec50" + integrity sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + "@babel/plugin-proposal-optional-chaining" "^7.18.9" + +"@babel/plugin-proposal-async-generator-functions@^7.20.1": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz#352f02baa5d69f4e7529bdac39aaa02d41146af9" + integrity sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-remap-async-to-generator" "^7.18.9" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-proposal-class-properties@^7.18.6", "@babel/plugin-proposal-class-properties@^7.8.3": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-class-static-block@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz#8aa81d403ab72d3962fc06c26e222dacfc9b9020" + integrity sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-proposal-decorators@^7.8.3": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.2.tgz#1c6c32b2a44b154ebeec2bb534f9eaebdb541fb6" + integrity sha512-nkBH96IBmgKnbHQ5gXFrcmez+Z9S2EIDKDQGp005ROqBigc88Tky4rzCnlP/lnlj245dCEQl4/YyV0V1kYh5dw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.20.2" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-replace-supers" "^7.19.1" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/plugin-syntax-decorators" "^7.19.0" + +"@babel/plugin-proposal-dynamic-import@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94" + integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-proposal-export-namespace-from@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" + integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b" + integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz#8148cbb350483bf6220af06fa6db3690e14b2e23" + integrity sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" + integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-proposal-numeric-separator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" + integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz#a556f59d555f06961df1e572bb5eca864c84022d" + integrity sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ== + dependencies: + "@babel/compat-data" "^7.20.1" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.20.1" + +"@babel/plugin-proposal-optional-catch-binding@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" + integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-proposal-optional-chaining@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz#e8e8fe0723f2563960e4bf5e9690933691915993" + integrity sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-proposal-private-methods@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" + integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-private-property-in-object@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz#a64137b232f0aca3733a67eb1a144c192389c503" + integrity sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" + integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-decorators@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz#5f13d1d8fce96951bea01a10424463c9a5b3a599" + integrity sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz#bb50e0d4bea0957235390641209394e87bdb9cc4" + integrity sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.2.0", "@babel/plugin-syntax-jsx@^7.8.3": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" + integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-arrow-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz#19063fcf8771ec7b31d742339dac62433d0611fe" + integrity sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-async-to-generator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz#ccda3d1ab9d5ced5265fdb13f1882d5476c71615" + integrity sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag== + dependencies: + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-remap-async-to-generator" "^7.18.6" + +"@babel/plugin-transform-block-scoped-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8" + integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-block-scoping@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.2.tgz#f59b1767e6385c663fd0bce655db6ca9c8b236ed" + integrity sha512-y5V15+04ry69OV2wULmwhEA6jwSWXO1TwAtIwiPXcvHcoOQUqpyMVd2bDsQJMW8AurjulIyUV8kDqtjSwHy1uQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-classes@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.2.tgz#c0033cf1916ccf78202d04be4281d161f6709bb2" + integrity sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-replace-supers" "^7.19.1" + "@babel/helper-split-export-declaration" "^7.18.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz#2357a8224d402dad623caf6259b611e56aec746e" + integrity sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-destructuring@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz#c23741cfa44ddd35f5e53896e88c75331b8b2792" + integrity sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" + integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-duplicate-keys@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e" + integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-exponentiation-operator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" + integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-for-of@^7.18.8": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz#6ef8a50b244eb6a0bdbad0c7c61877e4e30097c1" + integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-function-name@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" + integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== + dependencies: + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc" + integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-member-expression-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" + integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-modules-amd@^7.19.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz#aca391801ae55d19c4d8d2ebfeaa33df5f2a2cbd" + integrity sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg== + dependencies: + "@babel/helper-module-transforms" "^7.19.6" + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-transform-modules-commonjs@^7.19.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz#25b32feef24df8038fc1ec56038917eacb0b730c" + integrity sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ== + dependencies: + "@babel/helper-module-transforms" "^7.19.6" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-simple-access" "^7.19.4" + +"@babel/plugin-transform-modules-systemjs@^7.19.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz#59e2a84064b5736a4471b1aa7b13d4431d327e0d" + integrity sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ== + dependencies: + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-module-transforms" "^7.19.6" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-validator-identifier" "^7.19.1" + +"@babel/plugin-transform-modules-umd@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" + integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== + dependencies: + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz#ec7455bab6cd8fb05c525a94876f435a48128888" + integrity sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.19.0" + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-transform-new-target@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8" + integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-object-super@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" + integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-replace-supers" "^7.18.6" + +"@babel/plugin-transform-parameters@^7.20.1": + version "7.20.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.3.tgz#7b3468d70c3c5b62e46be0a47b6045d8590fb748" + integrity sha512-oZg/Fpx0YDrj13KsLyO8I/CX3Zdw7z0O9qOd95SqcoIzuqy/WTGWvePeHAnZCN54SfdyjHcb1S30gc8zlzlHcA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-property-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" + integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-regenerator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz#585c66cb84d4b4bf72519a34cfce761b8676ca73" + integrity sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + regenerator-transform "^0.15.0" + +"@babel/plugin-transform-reserved-words@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a" + integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-runtime@^7.11.0": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz#9d2a9dbf4e12644d6f46e5e75bfbf02b5d6e9194" + integrity sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw== + dependencies: + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.19.0" + babel-plugin-polyfill-corejs2 "^0.3.3" + babel-plugin-polyfill-corejs3 "^0.6.0" + babel-plugin-polyfill-regenerator "^0.4.1" + semver "^6.3.0" + +"@babel/plugin-transform-shorthand-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9" + integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-spread@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz#dd60b4620c2fec806d60cfaae364ec2188d593b6" + integrity sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + +"@babel/plugin-transform-sticky-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" + integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-template-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e" + integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-typeof-symbol@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0" + integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-unicode-escapes@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" + integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-unicode-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca" + integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/preset-env@^7.11.0": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.20.2.tgz#9b1642aa47bb9f43a86f9630011780dab7f86506" + integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg== + dependencies: + "@babel/compat-data" "^7.20.1" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-async-generator-functions" "^7.20.1" + "@babel/plugin-proposal-class-properties" "^7.18.6" + "@babel/plugin-proposal-class-static-block" "^7.18.6" + "@babel/plugin-proposal-dynamic-import" "^7.18.6" + "@babel/plugin-proposal-export-namespace-from" "^7.18.9" + "@babel/plugin-proposal-json-strings" "^7.18.6" + "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" + "@babel/plugin-proposal-numeric-separator" "^7.18.6" + "@babel/plugin-proposal-object-rest-spread" "^7.20.2" + "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" + "@babel/plugin-proposal-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-private-methods" "^7.18.6" + "@babel/plugin-proposal-private-property-in-object" "^7.18.6" + "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.20.0" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.18.6" + "@babel/plugin-transform-async-to-generator" "^7.18.6" + "@babel/plugin-transform-block-scoped-functions" "^7.18.6" + "@babel/plugin-transform-block-scoping" "^7.20.2" + "@babel/plugin-transform-classes" "^7.20.2" + "@babel/plugin-transform-computed-properties" "^7.18.9" + "@babel/plugin-transform-destructuring" "^7.20.2" + "@babel/plugin-transform-dotall-regex" "^7.18.6" + "@babel/plugin-transform-duplicate-keys" "^7.18.9" + "@babel/plugin-transform-exponentiation-operator" "^7.18.6" + "@babel/plugin-transform-for-of" "^7.18.8" + "@babel/plugin-transform-function-name" "^7.18.9" + "@babel/plugin-transform-literals" "^7.18.9" + "@babel/plugin-transform-member-expression-literals" "^7.18.6" + "@babel/plugin-transform-modules-amd" "^7.19.6" + "@babel/plugin-transform-modules-commonjs" "^7.19.6" + "@babel/plugin-transform-modules-systemjs" "^7.19.6" + "@babel/plugin-transform-modules-umd" "^7.18.6" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1" + "@babel/plugin-transform-new-target" "^7.18.6" + "@babel/plugin-transform-object-super" "^7.18.6" + "@babel/plugin-transform-parameters" "^7.20.1" + "@babel/plugin-transform-property-literals" "^7.18.6" + "@babel/plugin-transform-regenerator" "^7.18.6" + "@babel/plugin-transform-reserved-words" "^7.18.6" + "@babel/plugin-transform-shorthand-properties" "^7.18.6" + "@babel/plugin-transform-spread" "^7.19.0" + "@babel/plugin-transform-sticky-regex" "^7.18.6" + "@babel/plugin-transform-template-literals" "^7.18.9" + "@babel/plugin-transform-typeof-symbol" "^7.18.9" + "@babel/plugin-transform-unicode-escapes" "^7.18.10" + "@babel/plugin-transform-unicode-regex" "^7.18.6" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.20.2" + babel-plugin-polyfill-corejs2 "^0.3.3" + babel-plugin-polyfill-corejs3 "^0.6.0" + babel-plugin-polyfill-regenerator "^0.4.1" + core-js-compat "^3.25.1" + semver "^6.3.0" + +"@babel/preset-modules@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" + integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/runtime@^7.11.0", "@babel/runtime@^7.8.4": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" + integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg== + dependencies: + regenerator-runtime "^0.13.10" + +"@babel/template@^7.0.0", "@babel/template@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" + integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.10" + "@babel/types" "^7.18.10" + +"@babel/traverse@^7.0.0", "@babel/traverse@^7.19.0", "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8" + integrity sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.1" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.20.1" + "@babel/types" "^7.20.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.4.4": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.2.tgz#67ac09266606190f496322dbaff360fdaa5e7842" + integrity sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@csstools/selector-specificity@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" + integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== + +"@esbuild/android-arm@0.15.17": + version "0.15.17" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.17.tgz#05162390ed2b0f2ae9647a809efb71fba14ddfe7" + integrity sha512-ay6Ken4u+JStjYmqIgh71jMT0bs/rXpCCDKaMfl78B20QYWJglT5P6Ejfm4hWf6Zi+uUWNe7ZmqakRs2BQYIeg== + +"@esbuild/linux-loong64@0.15.17": + version "0.15.17" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.17.tgz#870daa61c257dfa0ee4f0ed71704d55e9af8e20c" + integrity sha512-IA1O7f7qxw2DX8oqTpugHElr926phs7Rq8ULXleBMk4go5K05BU0mI8BfCkWcYAvcmVaMc13bv5W3LIUlU6Y9w== + +"@eslint/eslintrc@^1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95" + integrity sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.15.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@humanwhocodes/config-array@^0.11.6": + version "0.11.7" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f" + integrity sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0", "@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@mrmlnc/readdir-enhanced@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" + integrity sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g== + dependencies: + call-me-maybe "^1.0.1" + glob-to-regexp "^0.3.0" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.stat@^1.1.2": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" + integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" + integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== + +"@types/archy@^0.0.31": + version "0.0.31" + resolved "https://registry.yarnpkg.com/@types/archy/-/archy-0.0.31.tgz#01650a4641e7e1d11dbd64eda42eec9a2f829c7f" + integrity sha512-v+dxizsFVyXgD3EpFuqT9YjdEjbJmPxNf1QIX9ohZOhxh1ZF2yhqv3vYaeum9lg3VghhxS5S0a6yldN9J9lPEQ== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect-history-api-fallback@*": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz#d1f7a8a09d0ed5a57aee5ae9c18ab9b803205dae" + integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/debug@^4.1.5": + version "4.1.7" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" + integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== + dependencies: + "@types/ms" "*" + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": + version "4.17.31" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f" + integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*": + version "4.17.14" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c" + integrity sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/fs-extra@^8.0.1": + version "8.1.2" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.2.tgz#7125cc2e4bdd9bd2fc83005ffdb1d0ba00cca61f" + integrity sha512-SvSrYXfWSc7R4eqnOzbQF4TZmfpNSM9FrSWLU3EUnWBuyZqNBOrv1B1JA3byUDPUl9z4Ab3jeZG2eDdySlgNMg== + dependencies: + "@types/node" "*" + +"@types/glob@*": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-8.0.0.tgz#321607e9cbaec54f687a0792b2d1d370739455d2" + integrity sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/glob@^7.1.1": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/highlight.js@^9.7.0": + version "9.12.4" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.4.tgz#8c3496bd1b50cc04aeefd691140aa571d4dbfa34" + integrity sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww== + +"@types/http-proxy@^1.17.5": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a" + integrity sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw== + dependencies: + "@types/node" "*" + +"@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/linkify-it@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.2.tgz#fd2cd2edbaa7eaac7e7f3c1748b52a19143846c9" + integrity sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA== + +"@types/lodash.debounce@^4.0.6": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f" + integrity sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.190" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.190.tgz#d8e99647af141c63902d0ca53cf2b34d2df33545" + integrity sha512-5iJ3FBJBvQHQ8sFhEhJfjUP+G+LalhavTkYyrAYqz5MEJG+erSv0k9KJLb6q7++17Lafk1scaTIFXcMJlwK8Mw== + +"@types/markdown-it@^10.0.0": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-10.0.3.tgz#a9800d14b112c17f1de76ec33eff864a4815eec7" + integrity sha512-daHJk22isOUvNssVGF2zDnnSyxHhFYhtjeX4oQaKD6QzL3ZR1QSgiD1g+Q6/WSWYVogNXYDXODtbgW/WiFCtyw== + dependencies: + "@types/highlight.js" "^9.7.0" + "@types/linkify-it" "*" + "@types/mdurl" "*" + highlight.js "^9.7.0" + +"@types/mdurl@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9" + integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA== + +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + +"@types/minimatch@*": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + +"@types/minimist@^1.2.0": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" + integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== + +"@types/mocha@^10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b" + integrity sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q== + +"@types/ms@*": + version "0.7.31" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + +"@types/node@*": + version "18.11.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" + integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== + +"@types/normalize-package-data@^2.4.0": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" + integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/q@^1.5.1": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" + integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/sass@^1.16.0": + version "1.43.1" + resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.1.tgz#86bb0168e9e881d7dade6eba16c9ed6d25dc2f68" + integrity sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g== + dependencies: + "@types/node" "*" + +"@types/semver@^7.3.12": + version "7.3.13" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" + integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== + +"@types/serve-static@*": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" + integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg== + dependencies: + "@types/mime" "*" + "@types/node" "*" + +"@types/source-list-map@*": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" + integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== + +"@types/tapable@^1": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" + integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ== + +"@types/three@^0.146.0": + version "0.146.0" + resolved "https://registry.yarnpkg.com/@types/three/-/three-0.146.0.tgz#83813ba0d2fff6bdc6d7fda3a77993a932bba45f" + integrity sha512-75AgysUrIvTCB054eQa2pDVFurfeFW8CrMQjpzjt3yHBfuuknoSvvsESd/3EhQxPrz9si3+P0wiDUVsWUlljfA== + dependencies: + "@types/webxr" "*" + +"@types/uglify-js@*": + version "3.17.1" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.17.1.tgz#e0ffcef756476410e5bce2cb01384ed878a195b5" + integrity sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g== + dependencies: + source-map "^0.6.1" + +"@types/webpack-dev-server@^3": + version "3.11.6" + resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.6.tgz#d8888cfd2f0630203e13d3ed7833a4d11b8a34dc" + integrity sha512-XCph0RiiqFGetukCTC3KVnY1jwLcZ84illFRMbyFzCcWl90B/76ew0tSqF46oBhnLC4obNDG7dMO0JfTN0MgMQ== + dependencies: + "@types/connect-history-api-fallback" "*" + "@types/express" "*" + "@types/serve-static" "*" + "@types/webpack" "^4" + http-proxy-middleware "^1.0.0" + +"@types/webpack-sources@*": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-3.2.0.tgz#16d759ba096c289034b26553d2df1bf45248d38b" + integrity sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg== + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.7.3" + +"@types/webpack@^4": + version "4.41.33" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.41.33.tgz#16164845a5be6a306bcbe554a8e67f9cac215ffc" + integrity sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g== + dependencies: + "@types/node" "*" + "@types/tapable" "^1" + "@types/uglify-js" "*" + "@types/webpack-sources" "*" + anymatch "^3.0.0" + source-map "^0.6.0" + +"@types/webxr@*": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.0.tgz#aae1cef3210d88fd4204f8c33385a0bbc4da07c9" + integrity sha512-IUMDPSXnYIbEO2IereEFcgcqfDREOgmbGqtrMpVPpACTU6pltYLwHgVkrnYv0XhWEcjio9sYEfIEzgn3c7nDqA== + +"@typescript-eslint/eslint-plugin@^5.46.0": + version "5.46.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.0.tgz#9a96a713b9616c783501a3c1774c9e2b40217ad0" + integrity sha512-QrZqaIOzJAjv0sfjY4EjbXUi3ZOFpKfzntx22gPGr9pmFcTjcFw/1sS1LJhEubfAGwuLjNrPV0rH+D1/XZFy7Q== + dependencies: + "@typescript-eslint/scope-manager" "5.46.0" + "@typescript-eslint/type-utils" "5.46.0" + "@typescript-eslint/utils" "5.46.0" + debug "^4.3.4" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + regexpp "^3.2.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.46.0": + version "5.46.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.46.0.tgz#002d8e67122947922a62547acfed3347cbf2c0b6" + integrity sha512-joNO6zMGUZg+C73vwrKXCd8usnsmOYmgW/w5ZW0pG0RGvqeznjtGDk61EqqTpNrFLUYBW2RSBFrxdAZMqA4OZA== + dependencies: + "@typescript-eslint/scope-manager" "5.46.0" + "@typescript-eslint/types" "5.46.0" + "@typescript-eslint/typescript-estree" "5.46.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.46.0": + version "5.46.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.46.0.tgz#60790b14d0c687dd633b22b8121374764f76ce0d" + integrity sha512-7wWBq9d/GbPiIM6SqPK9tfynNxVbfpihoY5cSFMer19OYUA3l4powA2uv0AV2eAZV6KoAh6lkzxv4PoxOLh1oA== + dependencies: + "@typescript-eslint/types" "5.46.0" + "@typescript-eslint/visitor-keys" "5.46.0" + +"@typescript-eslint/type-utils@5.46.0": + version "5.46.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.46.0.tgz#3a4507b3b437e2fd9e95c3e5eea5ae16f79d64b3" + integrity sha512-dwv4nimVIAsVS2dTA0MekkWaRnoYNXY26dKz8AN5W3cBFYwYGFQEqm/cG+TOoooKlncJS4RTbFKgcFY/pOiBCg== + dependencies: + "@typescript-eslint/typescript-estree" "5.46.0" + "@typescript-eslint/utils" "5.46.0" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.46.0": + version "5.46.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.46.0.tgz#f4d76622a996b88153bbd829ea9ccb9f7a5d28bc" + integrity sha512-wHWgQHFB+qh6bu0IAPAJCdeCdI0wwzZnnWThlmHNY01XJ9Z97oKqKOzWYpR2I83QmshhQJl6LDM9TqMiMwJBTw== + +"@typescript-eslint/typescript-estree@5.46.0": + version "5.46.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.0.tgz#a6c2b84b9351f78209a1d1f2d99ca553f7fa29a5" + integrity sha512-kDLNn/tQP+Yp8Ro2dUpyyVV0Ksn2rmpPpB0/3MO874RNmXtypMwSeazjEN/Q6CTp8D7ExXAAekPEcCEB/vtJkw== + dependencies: + "@typescript-eslint/types" "5.46.0" + "@typescript-eslint/visitor-keys" "5.46.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.46.0": + version "5.46.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.46.0.tgz#600cd873ba471b7d8b0b9f35de34cf852c6fcb31" + integrity sha512-4O+Ps1CRDw+D+R40JYh5GlKLQERXRKW5yIQoNDpmXPJ+C7kaPF9R7GWl+PxGgXjB3PQCqsaaZUpZ9dG4U6DO7g== + dependencies: + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.46.0" + "@typescript-eslint/types" "5.46.0" + "@typescript-eslint/typescript-estree" "5.46.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.46.0": + version "5.46.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.0.tgz#36d87248ae20c61ef72404bcd61f14aa2563915f" + integrity sha512-E13gBoIXmaNhwjipuvQg1ByqSAu/GbEpP/qzFihugJ+MomtoJtFAJG/+2DRPByf57B863m0/q7Zt16V9ohhANw== + dependencies: + "@typescript-eslint/types" "5.46.0" + eslint-visitor-keys "^3.3.0" + +"@vue/babel-helper-vue-jsx-merge-props@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz#8d53a1e21347db8edbe54d339902583176de09f2" + integrity sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA== + +"@vue/babel-helper-vue-transform-on@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz#9b9c691cd06fc855221a2475c3cc831d774bc7dc" + integrity sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA== + +"@vue/babel-plugin-jsx@^1.0.3": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz#0c5bac27880d23f89894cd036a37b55ef61ddfc1" + integrity sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/plugin-syntax-jsx" "^7.0.0" + "@babel/template" "^7.0.0" + "@babel/traverse" "^7.0.0" + "@babel/types" "^7.0.0" + "@vue/babel-helper-vue-transform-on" "^1.0.2" + camelcase "^6.0.0" + html-tags "^3.1.0" + svg-tags "^1.0.0" + +"@vue/babel-plugin-transform-vue-jsx@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.4.0.tgz#4d4b3d46a39ea62b7467dd6e26ce47f7ceafb2fe" + integrity sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA== + dependencies: + "@babel/helper-module-imports" "^7.0.0" + "@babel/plugin-syntax-jsx" "^7.2.0" + "@vue/babel-helper-vue-jsx-merge-props" "^1.4.0" + html-tags "^2.0.0" + lodash.kebabcase "^4.1.1" + svg-tags "^1.0.0" + +"@vue/babel-preset-app@^4.1.2": + version "4.5.19" + resolved "https://registry.yarnpkg.com/@vue/babel-preset-app/-/babel-preset-app-4.5.19.tgz#baee457da0065c016f74fac4149f7c97631ba5a7" + integrity sha512-VCNRiAt2P/bLo09rYt3DLe6xXUMlhJwrvU18Ddd/lYJgC7s8+wvhgYs+MTx4OiAXdu58drGwSBO9SPx7C6J82Q== + dependencies: + "@babel/core" "^7.11.0" + "@babel/helper-compilation-targets" "^7.9.6" + "@babel/helper-module-imports" "^7.8.3" + "@babel/plugin-proposal-class-properties" "^7.8.3" + "@babel/plugin-proposal-decorators" "^7.8.3" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-jsx" "^7.8.3" + "@babel/plugin-transform-runtime" "^7.11.0" + "@babel/preset-env" "^7.11.0" + "@babel/runtime" "^7.11.0" + "@vue/babel-plugin-jsx" "^1.0.3" + "@vue/babel-preset-jsx" "^1.2.4" + babel-plugin-dynamic-import-node "^2.3.3" + core-js "^3.6.5" + core-js-compat "^3.6.5" + semver "^6.1.0" + +"@vue/babel-preset-jsx@^1.2.4": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.4.0.tgz#f4914ba314235ab097bc4372ed67473c0780bfcc" + integrity sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA== + dependencies: + "@vue/babel-helper-vue-jsx-merge-props" "^1.4.0" + "@vue/babel-plugin-transform-vue-jsx" "^1.4.0" + "@vue/babel-sugar-composition-api-inject-h" "^1.4.0" + "@vue/babel-sugar-composition-api-render-instance" "^1.4.0" + "@vue/babel-sugar-functional-vue" "^1.4.0" + "@vue/babel-sugar-inject-h" "^1.4.0" + "@vue/babel-sugar-v-model" "^1.4.0" + "@vue/babel-sugar-v-on" "^1.4.0" + +"@vue/babel-sugar-composition-api-inject-h@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.4.0.tgz#187e1389f8871d89ece743bb50aed713be9d6c85" + integrity sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@vue/babel-sugar-composition-api-render-instance@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.4.0.tgz#2c1607ae6dffdab47e785bc01fa45ba756e992c1" + integrity sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@vue/babel-sugar-functional-vue@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.4.0.tgz#60da31068567082287c7337c66ef4df04e0a1029" + integrity sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@vue/babel-sugar-inject-h@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.4.0.tgz#bf39aa6631fb1d0399b1c49b4c59e1c8899b4363" + integrity sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + +"@vue/babel-sugar-v-model@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.4.0.tgz#a51d986609f430c4f70ada3a93cc560a2970f720" + integrity sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + "@vue/babel-helper-vue-jsx-merge-props" "^1.4.0" + "@vue/babel-plugin-transform-vue-jsx" "^1.4.0" + camelcase "^5.0.0" + html-tags "^2.0.0" + svg-tags "^1.0.0" + +"@vue/babel-sugar-v-on@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.4.0.tgz#43b7106a9672d8cbeefc0eb8afe1d376edc6166e" + integrity sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA== + dependencies: + "@babel/plugin-syntax-jsx" "^7.2.0" + "@vue/babel-plugin-transform-vue-jsx" "^1.4.0" + camelcase "^5.0.0" + +"@vue/compiler-sfc@2.7.14": + version "2.7.14" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz#3446fd2fbb670d709277fc3ffa88efc5e10284fd" + integrity sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA== + dependencies: + "@babel/parser" "^7.18.4" + postcss "^8.4.14" + source-map "^0.6.1" + +"@vue/component-compiler-utils@^3.1.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz#f9f5fb53464b0c37b2c8d2f3fbfe44df60f61dc9" + integrity sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ== + dependencies: + consolidate "^0.15.1" + hash-sum "^1.0.2" + lru-cache "^4.1.2" + merge-source-map "^1.1.0" + postcss "^7.0.36" + postcss-selector-parser "^6.0.2" + source-map "~0.6.1" + vue-template-es2015-compiler "^1.9.0" + optionalDependencies: + prettier "^1.18.2 || ^2.0.0" + +"@vuepress/core@1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/core/-/core-1.9.7.tgz#a23388377f84322b933fc97b6cca32a90d8f5ce2" + integrity sha512-u5eb1mfNLV8uG2UuxlvpB/FkrABxeMHqymTsixOnsOg2REziv9puEIbqaZ5BjLPvbCDvSj6rn+DwjENmBU+frQ== + dependencies: + "@babel/core" "^7.8.4" + "@vue/babel-preset-app" "^4.1.2" + "@vuepress/markdown" "1.9.7" + "@vuepress/markdown-loader" "1.9.7" + "@vuepress/plugin-last-updated" "1.9.7" + "@vuepress/plugin-register-components" "1.9.7" + "@vuepress/shared-utils" "1.9.7" + "@vuepress/types" "1.9.7" + autoprefixer "^9.5.1" + babel-loader "^8.0.4" + bundle-require "2.1.8" + cache-loader "^3.0.0" + chokidar "^2.0.3" + connect-history-api-fallback "^1.5.0" + copy-webpack-plugin "^5.0.2" + core-js "^3.6.4" + cross-spawn "^6.0.5" + css-loader "^2.1.1" + esbuild "0.14.7" + file-loader "^3.0.1" + js-yaml "^3.13.1" + lru-cache "^5.1.1" + mini-css-extract-plugin "0.6.0" + optimize-css-assets-webpack-plugin "^5.0.1" + portfinder "^1.0.13" + postcss-loader "^3.0.0" + postcss-safe-parser "^4.0.1" + toml "^3.0.0" + url-loader "^1.0.1" + vue "^2.6.10" + vue-loader "^15.7.1" + vue-router "^3.4.5" + vue-server-renderer "^2.6.10" + vue-template-compiler "^2.6.10" + vuepress-html-webpack-plugin "^3.2.0" + vuepress-plugin-container "^2.0.2" + webpack "^4.8.1" + webpack-chain "^6.0.0" + webpack-dev-server "^3.5.1" + webpack-merge "^4.1.2" + webpackbar "3.2.0" + +"@vuepress/markdown-loader@1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/markdown-loader/-/markdown-loader-1.9.7.tgz#acd4fa13f1e48f153d509996ccd2895a0dcb5ee2" + integrity sha512-mxXF8FtX/QhOg/UYbe4Pr1j5tcf/aOEI502rycTJ3WF2XAtOmewjkGV4eAA6f6JmuM/fwzOBMZKDyy9/yo2I6Q== + dependencies: + "@vuepress/markdown" "1.9.7" + loader-utils "^1.1.0" + lru-cache "^5.1.1" + +"@vuepress/markdown@1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/markdown/-/markdown-1.9.7.tgz#6310458b7e2ea08a14d31349209d0b54455e957a" + integrity sha512-DFOjYkwV6fT3xXTGdTDloeIrT1AbwJ9pwefmrp0rMgC6zOz3XUJn6qqUwcYFO5mNBWpbiFQ3JZirCtgOe+xxBA== + dependencies: + "@vuepress/shared-utils" "1.9.7" + markdown-it "^8.4.1" + markdown-it-anchor "^5.0.2" + markdown-it-chain "^1.3.0" + markdown-it-emoji "^1.4.0" + markdown-it-table-of-contents "^0.4.0" + prismjs "^1.13.0" + +"@vuepress/plugin-active-header-links@1.9.7", "@vuepress/plugin-active-header-links@^1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/plugin-active-header-links/-/plugin-active-header-links-1.9.7.tgz#11b3b148d50ebd0a9a9d9e97aa34d81ae04e7307" + integrity sha512-G1M8zuV9Og3z8WBiKkWrofG44NEXsHttc1MYreDXfeWh/NLjr9q1GPCEXtiCjrjnHZHB3cSQTKnTqAHDq35PGA== + dependencies: + "@vuepress/types" "1.9.7" + lodash.debounce "^4.0.8" + +"@vuepress/plugin-back-to-top@^1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/plugin-back-to-top/-/plugin-back-to-top-1.9.7.tgz#249a76d79f1e0c8c71a2804485ad0245837e6bfd" + integrity sha512-DM1S+Q8Xn/i+zhe4zThekxb1M2abfKLklg/NKtQloklHKdNdVfk+EcxWYNmNfSii+ymDWaaG8lmH0xjVhy0iXw== + dependencies: + "@vuepress/types" "1.9.7" + lodash.debounce "^4.0.8" + +"@vuepress/plugin-google-analytics@^1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/plugin-google-analytics/-/plugin-google-analytics-1.9.7.tgz#0162e7bb28686422298c04a12f2e7727a2708d90" + integrity sha512-ZpsYrk23JdwbcJo9xArVcdqYHt5VyTX9UN9bLqNrLJRgRTV0X2jKUkM63dlKTJMpBf+0K1PQMJbGBXgOO7Yh0Q== + dependencies: + "@vuepress/types" "1.9.7" + +"@vuepress/plugin-last-updated@1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/plugin-last-updated/-/plugin-last-updated-1.9.7.tgz#9f2d78fe7ced618d0480bf40a3e32b40486bac6d" + integrity sha512-FiFBOl49dlFRjbLRnRAv77HDWfe+S/eCPtMQobq4/O3QWuL3Na5P4fCTTVzq1K7rWNO9EPsWNB2Jb26ndlQLKQ== + dependencies: + "@vuepress/types" "1.9.7" + cross-spawn "^6.0.5" + +"@vuepress/plugin-nprogress@1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/plugin-nprogress/-/plugin-nprogress-1.9.7.tgz#76d8368fa26c190ee23c399401a71ec78ffb9744" + integrity sha512-sI148igbdRfLgyzB8PdhbF51hNyCDYXsBn8bBWiHdzcHBx974sVNFKtfwdIZcSFsNrEcg6zo8YIrQ+CO5vlUhQ== + dependencies: + "@vuepress/types" "1.9.7" + nprogress "^0.2.0" + +"@vuepress/plugin-register-components@1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/plugin-register-components/-/plugin-register-components-1.9.7.tgz#0234f887b32c1d836fa68cdd06d7e851397fd268" + integrity sha512-l/w1nE7Dpl+LPMb8+AHSGGFYSP/t5j6H4/Wltwc2QcdzO7yqwC1YkwwhtTXvLvHOV8O7+rDg2nzvq355SFkfKA== + dependencies: + "@vuepress/shared-utils" "1.9.7" + "@vuepress/types" "1.9.7" + +"@vuepress/plugin-search@1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/plugin-search/-/plugin-search-1.9.7.tgz#37a4714973ccac8c28837fc72a38ae0888d874bf" + integrity sha512-MLpbUVGLxaaHEwflFxvy0pF9gypFVUT3Q9Zc6maWE+0HDWAvzMxo6GBaj6mQPwjOqNQMf4QcN3hDzAZktA+DQg== + dependencies: + "@vuepress/types" "1.9.7" + +"@vuepress/shared-utils@1.9.7", "@vuepress/shared-utils@^1.2.0": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/shared-utils/-/shared-utils-1.9.7.tgz#f1203c7f48e9d546078f5f9b2ec5200b29da481b" + integrity sha512-lIkO/eSEspXgVHjYHa9vuhN7DuaYvkfX1+TTJDiEYXIwgwqtvkTv55C+IOdgswlt0C/OXDlJaUe1rGgJJ1+FTw== + dependencies: + chalk "^2.3.2" + escape-html "^1.0.3" + fs-extra "^7.0.1" + globby "^9.2.0" + gray-matter "^4.0.1" + hash-sum "^1.0.2" + semver "^6.0.0" + toml "^3.0.0" + upath "^1.1.0" + +"@vuepress/theme-default@1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/theme-default/-/theme-default-1.9.7.tgz#9e928b724fdcb12715cc513fdbc27b965944c4a1" + integrity sha512-NZzCLIl+bgJIibhkqVmk/NSku57XIuXugxAN3uiJrCw6Mu6sb3xOvbk0En3k+vS2BKHxAZ6Cx7dbCiyknDQnSA== + dependencies: + "@vuepress/plugin-active-header-links" "1.9.7" + "@vuepress/plugin-nprogress" "1.9.7" + "@vuepress/plugin-search" "1.9.7" + "@vuepress/types" "1.9.7" + docsearch.js "^2.5.2" + lodash "^4.17.15" + stylus "^0.54.8" + stylus-loader "^3.0.2" + vuepress-plugin-container "^2.0.2" + vuepress-plugin-smooth-scroll "^0.0.3" + +"@vuepress/types@1.9.7": + version "1.9.7" + resolved "https://registry.yarnpkg.com/@vuepress/types/-/types-1.9.7.tgz#aeb772fd0f7c2a10c6ec1d3c803a2e4b1d756c24" + integrity sha512-moLQzkX3ED2o18dimLemUm7UVDKxhcrJmGt5C0Ng3xxrLPaQu7UqbROtEKB3YnMRt4P/CA91J+Ck+b9LmGabog== + dependencies: + "@types/markdown-it" "^10.0.0" + "@types/webpack-dev-server" "^3" + webpack-chain "^6.0.0" + +"@webassemblyjs/ast@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" + integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== + dependencies: + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + +"@webassemblyjs/floating-point-hex-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" + integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== + +"@webassemblyjs/helper-api-error@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" + integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== + +"@webassemblyjs/helper-buffer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" + integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== + +"@webassemblyjs/helper-code-frame@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" + integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== + dependencies: + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/helper-fsm@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" + integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== + +"@webassemblyjs/helper-module-context@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" + integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== + dependencies: + "@webassemblyjs/ast" "1.9.0" + +"@webassemblyjs/helper-wasm-bytecode@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" + integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== + +"@webassemblyjs/helper-wasm-section@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" + integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + +"@webassemblyjs/ieee754@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" + integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" + integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" + integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== + +"@webassemblyjs/wasm-edit@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" + integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/helper-wasm-section" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-opt" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/wasm-gen@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" + integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wasm-opt@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" + integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + +"@webassemblyjs/wasm-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" + integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wast-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" + integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/floating-point-hex-parser" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-code-frame" "1.9.0" + "@webassemblyjs/helper-fsm" "1.9.0" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/wast-printer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" + integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^6.4.1: + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== + +acorn@^8.4.1, acorn@^8.8.0: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + +agentkeepalive@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-2.2.0.tgz#c5d1bd4b129008f1163f236f86e5faea2026e2ef" + integrity sha512-TnB6ziK363p7lR8QpeLC8aMr8EGYBKZTpgzQLfqTs3bR0Oo5VbKdwKf8h0dSzsYrB7lSCgfJnMZKqShvlq5Oyg== + +ajv-errors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" + integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== + +ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.1: + version "8.11.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.2.tgz#aecb20b50607acf2569b6382167b65a96008bb78" + integrity sha512-E4bfmKAhGiSTvMfL1Myyycaub+cUEU2/IvpylXkUu7CHBkBj1f/ikdzbD7YQ6FKUbixDxeYvB/xY4fvyroDlQg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +algoliasearch@^3.24.5: + version "3.35.1" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.35.1.tgz#297d15f534a3507cab2f5dfb996019cac7568f0c" + integrity sha512-K4yKVhaHkXfJ/xcUnil04xiSrB8B8yHZoFEhWNpXg23eiCnqvTZw1tn/SqvdsANlYHLJlKl0qi3I/Q2Sqo7LwQ== + dependencies: + agentkeepalive "^2.2.0" + debug "^2.6.9" + envify "^4.0.0" + es6-promise "^4.1.0" + events "^1.1.0" + foreach "^2.0.5" + global "^4.3.2" + inherits "^2.0.1" + isarray "^2.0.1" + load-script "^1.0.0" + object-keys "^1.0.11" + querystring-es3 "^0.2.1" + reduce "^1.0.1" + semver "^5.1.0" + tunnel-agent "^0.6.0" + +alive-server@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/alive-server/-/alive-server-1.2.9.tgz#0759ab9bd782cc4bacbb84b96d3c5e7bf52262ae" + integrity sha512-LDQrwUkiLNwyL6tO4Xv31roMCWJ0FdiQ2EjX65YLrmsmUimasYJ3uLLigYRqJKDCcZmZjTfyW3mZ0zDfcbw7Fw== + dependencies: + chokidar "^3.5.2" + colors "1.4.0" + connect "^3.6.6" + cors latest + event-stream "3.3.4" + faye-websocket "0.11.x" + http-auth "3.1.x" + morgan "^1.9.1" + object-assign latest + open latest + proxy-middleware latest + send latest + serve-index "^1.9.1" + +alphanum-sort@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + integrity sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ== + +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-colors@^3.0.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + +ansi-escapes@^4.1.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-html-community@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +anymatch@^3.0.0, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +apache-crypt@^1.1.2: + version "1.2.6" + resolved "https://registry.yarnpkg.com/apache-crypt/-/apache-crypt-1.2.6.tgz#c3f9b98318b447f0a878b54e2cb113bbb8539698" + integrity sha512-072WetlM4blL8PREJVeY+WHiUh1R5VNt2HfceGS8aKqttPHcmqE5pkKuXPz/ULmJOFkc8Hw3kfKl6vy7Qka6DA== + dependencies: + unix-crypt-td-js "^1.1.4" + +apache-md5@^1.0.6: + version "1.1.8" + resolved "https://registry.yarnpkg.com/apache-md5/-/apache-md5-1.1.8.tgz#ea79c6feb03abfed42b2830dde06f75df5e3bbd9" + integrity sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA== + +aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +archy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA== + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q== + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-union@^1.0.1, array-union@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng== + dependencies: + array-uniq "^1.0.1" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ== + +array.prototype.reduce@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac" + integrity sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-array-method-boxes-properly "^1.0.0" + is-string "^1.0.7" + +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== + +asn1.js@^5.2.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + +assert@^1.1.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" + integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== + dependencies: + object-assign "^4.1.1" + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw== + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async@^2.6.4: + version "2.6.4" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" + integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== + dependencies: + lodash "^4.17.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +autocomplete.js@0.36.0: + version "0.36.0" + resolved "https://registry.yarnpkg.com/autocomplete.js/-/autocomplete.js-0.36.0.tgz#94fe775fe64b6cd42e622d076dc7fd26bedd837b" + integrity sha512-jEwUXnVMeCHHutUt10i/8ZiRaCb0Wo+ZyKxeGsYwBDtw6EJHqEeDrq4UwZRD8YBSvp3g6klP678il2eeiVXN2Q== + dependencies: + immediate "^3.2.3" + +autoprefixer@^9.5.1: + version "9.8.8" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a" + integrity sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA== + dependencies: + browserslist "^4.12.0" + caniuse-lite "^1.0.30001109" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + picocolors "^0.2.1" + postcss "^7.0.32" + postcss-value-parser "^4.1.0" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +babel-loader@^8.0.4: + version "8.3.0" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" + integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^2.0.0" + make-dir "^3.1.0" + schema-utils "^2.6.5" + +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== + dependencies: + object.assign "^4.1.0" + +babel-plugin-polyfill-corejs2@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz#5d1bd3836d0a19e1b84bbf2d9640ccb6f951c122" + integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== + dependencies: + "@babel/compat-data" "^7.17.7" + "@babel/helper-define-polyfill-provider" "^0.3.3" + semver "^6.1.1" + +babel-plugin-polyfill-corejs3@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a" + integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + core-js-compat "^3.25.1" + +babel-plugin-polyfill-regenerator@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747" + integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +balanced-match@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" + integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== + +base64-js@^1.0.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +bcryptjs@^2.3.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ== + +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q== + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +binaryextensions@^2.1.2: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22" + integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg== + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bluebird@^3.1.1, bluebird@^3.5.5: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.0.0, bn.js@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + integrity sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg== + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +boxen@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" + integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^5.3.1" + chalk "^3.0.0" + cli-boxes "^2.2.0" + string-width "^4.1.0" + term-size "^2.1.0" + type-fest "^0.8.1" + widest-line "^3.1.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.0.1, brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" + integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== + dependencies: + bn.js "^5.0.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" + integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== + dependencies: + bn.js "^5.1.1" + browserify-rsa "^4.0.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.3" + inherits "^2.0.4" + parse-asn1 "^5.1.5" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.21.3, browserslist@^4.21.4: + version "4.21.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" + integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== + dependencies: + caniuse-lite "^1.0.30001400" + electron-to-chromium "^1.4.251" + node-releases "^2.0.6" + update-browserslist-db "^1.0.9" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + +buffer-json@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/buffer-json/-/buffer-json-2.0.0.tgz#f73e13b1e42f196fe2fd67d001c7d7107edd7c23" + integrity sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== + +buffer@^4.3.0: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== + +bundle-require@2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-2.1.8.tgz#28f6de9d4468a6b7b76fb5c9bf52e70f5091245d" + integrity sha512-oOEg3A0hy/YzvNWNowtKD0pmhZKseOFweCbgyMqTIih4gRY1nJWsvrOCT27L9NbIyL5jMjTFrAUpGxxpW68Puw== + +bundle-require@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-3.1.2.tgz#1374a7bdcb8b330a7ccc862ccbf7c137cc43ad27" + integrity sha512-Of6l6JBAxiyQ5axFxUM6dYeP/W7X2Sozeo/4EYB9sJhL+dqL7TKjg+shwxp6jlu/6ZSERfsYtIpSJ1/x3XkAEA== + dependencies: + load-tsconfig "^0.2.0" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cac@^6.5.6, cac@^6.7.12: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + +cacache@^12.0.2, cacache@^12.0.3: + version "12.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" + integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== + dependencies: + bluebird "^3.5.5" + chownr "^1.1.1" + figgy-pudding "^3.5.1" + glob "^7.1.4" + graceful-fs "^4.1.15" + infer-owner "^1.0.3" + lru-cache "^5.1.1" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.3" + ssri "^6.0.1" + unique-filename "^1.1.1" + y18n "^4.0.0" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +cache-loader@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cache-loader/-/cache-loader-3.0.1.tgz#cee6cf4b3cdc7c610905b26bad6c2fc439c821af" + integrity sha512-HzJIvGiGqYsFUrMjAJNDbVZoG7qQA+vy9AIoKs7s9DscNfki0I589mf2w6/tW+kkFH3zyiknoWV5Jdynu6b/zw== + dependencies: + buffer-json "^2.0.0" + find-cache-dir "^2.1.0" + loader-utils "^1.2.3" + mkdirp "^0.5.1" + neo-async "^2.6.1" + schema-utils "^1.0.0" + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ== + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A== + dependencies: + caller-callsite "^2.0.0" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@3.0.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + integrity sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w== + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + +camelcase-keys@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" + integrity sha512-Ej37YKYbFUI8QiYlvj9YHb6/Z60dZyPJW0Cs8sFilMbd2lP0bw3ylAq9yJkK4lcTA2dID5fG8LjmJYbO7kWb7Q== + dependencies: + camelcase "^4.1.0" + map-obj "^2.0.0" + quick-lru "^1.0.0" + +camelcase-keys@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" + integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== + dependencies: + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw== + +camelcase@^5.0.0, camelcase@^5.2.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001400: + version "1.0.30001434" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz#ec1ec1cfb0a93a34a0600d37903853030520a4e5" + integrity sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + +chalk@^2.0.0, chalk@^2.3.2, chalk@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@3.5.3, "chokidar@>=3.0.0 <4.0.0", chokidar@^3.3.1, chokidar@^3.4.1, chokidar@^3.5.1, chokidar@^3.5.2: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^2.0.3, chokidar@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +ci-info@^3.1.1: + version "3.7.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.0.tgz#6d01b3696c59915b6ce057e4aa4adfc2fa25f5ef" + integrity sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog== + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +clean-css@4.2.x: + version "4.2.4" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178" + integrity sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A== + dependencies: + source-map "~0.6.0" + +cli-boxes@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-response@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== + dependencies: + mimic-response "^1.0.0" + +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" + integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +codesandbox-import-util-types@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/codesandbox-import-util-types/-/codesandbox-import-util-types-2.2.3.tgz#b354b2f732ad130e119ebd9ead3bda3be5981a54" + integrity sha512-Qj00p60oNExthP2oR3vvXmUGjukij+rxJGuiaKM6tyUmSyimdZsqHI/TUvFFClAffk9s7hxGnQgWQ8KCce27qQ== + +codesandbox-import-utils@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/codesandbox-import-utils/-/codesandbox-import-utils-2.2.3.tgz#f7b4801245b381cb8c90fe245e336624e19b6c84" + integrity sha512-ymtmcgZKU27U+nM2qUb21aO8Ut/u2S9s6KorOgG81weP+NA0UZkaHKlaRqbLJ9h4i/4FLvwmEXYAnTjNmp6ogg== + dependencies: + codesandbox-import-util-types "^2.2.3" + istextorbinary "^2.2.1" + lz-string "^1.4.4" + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw== + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0, color-convert@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.6.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +colord@^2.9.3: + version "2.9.3" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== + +colors@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@2.17.x: + version "2.17.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" + integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^4.0.0, commander@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@~2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" + integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== + dependencies: + dot-prop "^5.2.0" + graceful-fs "^4.1.2" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" + +connect-history-api-fallback@^1.5.0, connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +connect@^3.6.6: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +consola@^2.6.0: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +console-browserify@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" + integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== + +consolidate@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7" + integrity sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw== + dependencies: + bluebird "^3.1.1" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-source-map@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== + +copy-webpack-plugin@^5.0.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.1.2.tgz#8a889e1dcafa6c91c6cd4be1ad158f1d3823bae2" + integrity sha512-Uh7crJAco3AjBvgAy9Z75CjK8IG+gxaErro71THQ+vv/bl4HaQcpkexAY8KVW/T6D2W2IRr+couF/knIRkZMIQ== + dependencies: + cacache "^12.0.3" + find-cache-dir "^2.1.0" + glob-parent "^3.1.0" + globby "^7.1.1" + is-glob "^4.0.1" + loader-utils "^1.2.3" + minimatch "^3.0.4" + normalize-path "^3.0.0" + p-limit "^2.2.1" + schema-utils "^1.0.0" + serialize-javascript "^4.0.0" + webpack-log "^2.0.0" + +core-js-compat@^3.25.1, core-js-compat@^3.6.5: + version "3.26.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.26.1.tgz#0e710b09ebf689d719545ac36e49041850f943df" + integrity sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A== + dependencies: + browserslist "^4.21.4" + +core-js@^3.6.4, core-js@^3.6.5: + version "3.26.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.26.1.tgz#7a9816dabd9ee846c1c0fe0e8fcad68f3709134e" + integrity sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA== + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@latest: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cosmiconfig@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +cosmiconfig@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +create-ecdh@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" + integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== + dependencies: + bn.js "^4.1.0" + elliptic "^6.5.3" + +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +cross-spawn@^6.0.0, cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +css-color-names@0.0.4, css-color-names@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + integrity sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q== + +css-declaration-sorter@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" + integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== + dependencies: + postcss "^7.0.1" + timsort "^0.3.0" + +css-functions-list@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.1.0.tgz#cf5b09f835ad91a00e5959bcfc627cd498e1321b" + integrity sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w== + +css-loader@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea" + integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w== + dependencies: + camelcase "^5.2.0" + icss-utils "^4.1.0" + loader-utils "^1.2.3" + normalize-path "^3.0.0" + postcss "^7.0.14" + postcss-modules-extract-imports "^2.0.0" + postcss-modules-local-by-default "^2.0.6" + postcss-modules-scope "^2.1.0" + postcss-modules-values "^2.0.0" + postcss-value-parser "^3.3.0" + schema-utils "^1.0.0" + +css-parse@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-2.0.0.tgz#a468ee667c16d81ccf05c58c38d2a97c780dbfd4" + integrity sha512-UNIFik2RgSbiTwIW1IsFwXWn6vs+bYdq83LKTSOsx7NJR7WII9dxewkHLltfTLVppoUApHV0118a4RZRI9FLwA== + dependencies: + css "^2.0.0" + +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== + +css-select@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== + dependencies: + boolbase "^1.0.0" + css-what "^3.2.1" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-select@^4.1.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-tree@1.0.0-alpha.37: + version "1.0.0-alpha.37" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" + integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== + dependencies: + mdn-data "2.0.4" + source-map "^0.6.1" + +css-tree@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^3.2.1: + version "3.4.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== + +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +css@^2.0.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" + integrity sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== + dependencies: + inherits "^2.0.3" + source-map "^0.6.1" + source-map-resolve "^0.5.2" + urix "^0.1.0" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz#920622b1fc1e95a34e8838203f1397a504f2d3ff" + integrity sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ== + dependencies: + css-declaration-sorter "^4.0.1" + cssnano-util-raw-cache "^4.0.1" + postcss "^7.0.0" + postcss-calc "^7.0.1" + postcss-colormin "^4.0.3" + postcss-convert-values "^4.0.1" + postcss-discard-comments "^4.0.2" + postcss-discard-duplicates "^4.0.2" + postcss-discard-empty "^4.0.1" + postcss-discard-overridden "^4.0.1" + postcss-merge-longhand "^4.0.11" + postcss-merge-rules "^4.0.3" + postcss-minify-font-values "^4.0.2" + postcss-minify-gradients "^4.0.2" + postcss-minify-params "^4.0.2" + postcss-minify-selectors "^4.0.2" + postcss-normalize-charset "^4.0.1" + postcss-normalize-display-values "^4.0.2" + postcss-normalize-positions "^4.0.2" + postcss-normalize-repeat-style "^4.0.2" + postcss-normalize-string "^4.0.2" + postcss-normalize-timing-functions "^4.0.2" + postcss-normalize-unicode "^4.0.1" + postcss-normalize-url "^4.0.1" + postcss-normalize-whitespace "^4.0.2" + postcss-ordered-values "^4.1.2" + postcss-reduce-initial "^4.0.3" + postcss-reduce-transforms "^4.0.2" + postcss-svgo "^4.0.3" + postcss-unique-selectors "^4.0.1" + +cssnano-util-get-arguments@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" + integrity sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw== + +cssnano-util-get-match@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" + integrity sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw== + +cssnano-util-raw-cache@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" + integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== + dependencies: + postcss "^7.0.0" + +cssnano-util-same-parent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" + integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== + +cssnano@^4.1.10: + version "4.1.11" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.11.tgz#c7b5f5b81da269cb1fd982cb960c1200910c9a99" + integrity sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g== + dependencies: + cosmiconfig "^5.0.0" + cssnano-preset-default "^4.0.8" + is-resolvable "^1.0.0" + postcss "^7.0.0" + +csso@^4.0.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +csstype@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng== + dependencies: + array-find-index "^1.0.1" + +cyclist@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" + integrity sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + +date-fns@^2.29.3: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + +de-indent@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" + integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +decamelize-keys@^1.0.0, decamelize-keys@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" + integrity sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg== + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0, decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== + dependencies: + mimic-response "^1.0.0" + +deep-equal@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753" + integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ== + +default-gateway@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== + dependencies: + execa "^1.0.0" + ip-regex "^2.1.0" + +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.2, define-properties@^1.1.3, define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA== + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA== + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +del@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +des.js@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" + integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA== + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-indent@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.1.tgz#cbb060a12842b9c4d333f1cac4aa4da1bb66bc25" + integrity sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g== + +detect-newline@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-4.0.0.tgz#450ac3f864d5f61112b53a524123b012c59581bc" + integrity sha512-1aXUEPdfGdzVPFpzGJJNgq9o81bGg1s09uxTWsqBlo9PI332uyJRQq13+LK/UN4JfxJbFdCXonUFQ9R/p7yCtw== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dir-glob@^2.0.0, dir-glob@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" + integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== + dependencies: + path-type "^3.0.0" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== + +dns-packet@^1.3.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.4.tgz#e3455065824a2507ba886c55a89963bb107dec6f" + integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ== + dependencies: + buffer-indexof "^1.0.0" + +docsearch.js@^2.5.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/docsearch.js/-/docsearch.js-2.6.3.tgz#57cb4600d3b6553c677e7cbbe6a734593e38625d" + integrity sha512-GN+MBozuyz664ycpZY0ecdQE0ND/LSgJKhTLA0/v3arIS3S1Rpf2OJz6A35ReMsm91V5apcmzr5/kM84cvUg+A== + dependencies: + algoliasearch "^3.24.5" + autocomplete.js "0.36.0" + hogan.js "^3.0.2" + request "^2.87.0" + stack-utils "^1.0.1" + to-factory "^1.0.0" + zepto "^1.2.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-converter@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +dom-walk@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" + integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== + +domelementtype@1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^2.5.2, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +duplexer3@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" + integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== + +duplexer@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +duplexify@^3.4.2, duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +editions@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/editions/-/editions-2.3.1.tgz#3bc9962f1978e801312fbd0aebfed63b49bfe698" + integrity sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA== + dependencies: + errlop "^2.0.0" + semver "^6.3.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.251: + version "1.4.284" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" + integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== + +elliptic@^6.5.3: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + integrity sha512-knHEZMgs8BB+MInokmNTg/OyPlAddghe1YBgNwJBc5zsJi/uyIcXoSDsL/W9ymOsBoBGdPIHXYJ9+qKFwRwDng== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" + integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.5.0" + tapable "^1.0.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + +envify@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/envify/-/envify-4.1.0.tgz#f39ad3db9d6801b4e6b478b61028d3f0b6819f7e" + integrity sha512-IKRVVoAYr4pIx4yIWNsz9mOsboxlNXiu7TNBnem/K/uTHdkyzXWDzHCK7UTolqBbgaBz0tQHsD3YNls0uIIjiw== + dependencies: + esprima "^4.0.0" + through "~2.3.4" + +envinfo@^7.2.0: + version "7.8.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + +errlop@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/errlop/-/errlop-2.2.0.tgz#1ff383f8f917ae328bebb802d6ca69666a42d21b" + integrity sha512-e64Qj9+4aZzjzzFpZC7p5kmm/ccCrbLhAJplhsDXQFs87XTsXwOpH4s1Io2s90Tau/8r2j9f4l/thhDevRjzxw== + +errno@^0.1.3, errno@~0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" + integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== + dependencies: + prr "~1.0.1" + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.20.4: + version "1.20.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.4.tgz#1d103f9f8d78d4cf0713edcd6d0ed1a46eed5861" + integrity sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.3" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" + internal-slot "^1.0.3" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-weakref "^1.0.2" + object-inspect "^1.12.2" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + safe-regex-test "^1.0.0" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-array-method-boxes-properly@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" + integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es6-promise@^4.1.0: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +esbuild-android-64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.17.tgz#3e639a064b275a5e9aaa02a2f4ea0e1d2d875fc9" + integrity sha512-sUs6cKMAuAyWnJ/66ezWVr9SMRGFSwoMagxzdhXYggSA12zF7krXSuc1Y9JwxHq56wtv/gFAVo97TFm7RBc1Ig== + +esbuild-android-arm64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.7.tgz#8c78cbb617f9f216abfb5a84cca453b51421a1b6" + integrity sha512-9/Q1NC4JErvsXzJKti0NHt+vzKjZOgPIjX/e6kkuCzgfT/GcO3FVBcGIv4HeJG7oMznE6KyKhvLrFgt7CdU2/w== + +esbuild-android-arm64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.17.tgz#c863d99723e219bda5336352002530d3fdcc62bf" + integrity sha512-RLZuCgIx1rexwxwsXTEW40ZiZzdBI1MBphwDRFyms/iiJGwLxqCH7v75iSJk5s6AF6oa80KC6r/RmzyaX/uJNg== + +esbuild-darwin-64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.7.tgz#7424bdb64c104556d36b7429af79ab51415ab8f4" + integrity sha512-Z9X+3TT/Xj+JiZTVlwHj2P+8GoiSmUnGVz0YZTSt8WTbW3UKw5Pw2ucuJ8VzbD2FPy0jbIKJkko/6CMTQchShQ== + +esbuild-darwin-64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.17.tgz#2c68c9cf85235a5e79b3936c12dfee1ce4aafa55" + integrity sha512-+6RTCZ0hfAb+RqTNq1uVsBcP441yZOSi6CyV9BIBryGGVg8RM3Bc6L45e5b68jdRloddN92ekS50e4ElI+cHQA== + +esbuild-darwin-arm64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.7.tgz#6a243dc0132aeb11c1991f968a6a9e393f43c6bc" + integrity sha512-68e7COhmwIiLXBEyxUxZSSU0akgv8t3e50e2QOtKdBUE0F6KIRISzFntLe2rYlNqSsjGWsIO6CCc9tQxijjSkw== + +esbuild-darwin-arm64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.17.tgz#3d277785acea953e0bb23e21c5df536341055aad" + integrity sha512-ne4UWUHEKWLgYSE5SLr0/TBcID3k9LPnrzzRXzFLTfD+ygjnW1pMEgdMfmOKIe8jYBUYv8x/YoksriTdQb9r/Q== + +esbuild-freebsd-64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.7.tgz#e7281e50522e724c4da502504dcd75be0db46c94" + integrity sha512-76zy5jAjPiXX/S3UvRgG85Bb0wy0zv/J2lel3KtHi4V7GUTBfhNUPt0E5bpSXJ6yMT7iThhnA5rOn+IJiUcslQ== + +esbuild-freebsd-64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.17.tgz#d8727eada6b84ad27be448ec9da461497a497289" + integrity sha512-6my3DrwLOe1zhR8UzVRKeo9AFM9XkApJBcx0IE+qKaEbKKBxYAiDBtd2ZMtRA2agqIwRP0kuHofTiDEzpfA+ZA== + +esbuild-freebsd-arm64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.7.tgz#31e513098efd181d76a3ba3ea285836d37f018a3" + integrity sha512-lSlYNLiqyzd7qCN5CEOmLxn7MhnGHPcu5KuUYOG1i+t5A6q7LgBmfYC9ZHJBoYyow3u4CNu79AWHbvVLpE/VQQ== + +esbuild-freebsd-arm64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.17.tgz#441ee277b908e068f5780f829c13d999328143ab" + integrity sha512-LQL7+f+bz+xmAu1FcDBB304Wm2CjONUcOeF4f3TqG7wYXMxjjYQZBFv+0OVapNXyYrM2vy9JMDbps+SheuOnHg== + +esbuild-linux-32@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.7.tgz#82cf96accbf55d3007c3338dc3b3144efa9ae108" + integrity sha512-Vk28u409wVOXqTaT6ek0TnfQG4Ty1aWWfiysIaIRERkNLhzLhUf4i+qJBN8mMuGTYOkE40F0Wkbp6m+IidOp2A== + +esbuild-linux-32@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.17.tgz#6109df781f8f5a02792d6aa457a727485b9972c7" + integrity sha512-7E9vZXMZhINQ4/KcxBxioJ2ao5gbXJ6Pa4/LEUd102g3gadSalpg0LrityFgw7ao6qmjcNWwdEYrXaDnOzyyYA== + +esbuild-linux-64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.7.tgz#67bdfe23a6ca918a0bb8e9558a3ee0fdf98c0bc0" + integrity sha512-+Lvz6x+8OkRk3K2RtZwO+0a92jy9si9cUea5Zoru4yJ/6EQm9ENX5seZE0X9DTwk1dxJbjmLsJsd3IoowyzgVg== + +esbuild-linux-64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.17.tgz#67a9f35edcf45768d7f460c3981a2c8ec14274e2" + integrity sha512-TnedHtFQSUVlc0J0D4ZMMalYaQ0Zbt7HSwGy4sav7BlXVqDVc/rchJ/a9dathK51apzLgRyXQMseLf6bkloaSQ== + +esbuild-linux-arm64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.7.tgz#f79c69ff0c176559c418de8e59aa3cf388fff992" + integrity sha512-kJd5beWSqteSAW086qzCEsH6uwpi7QRIpzYWHzEYwKKu9DiG1TwIBegQJmLpPsLp4v5RAFjea0JAmAtpGtRpqg== + +esbuild-linux-arm64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.17.tgz#8f0277990429c0ded773d704ce94eec969d5eca6" + integrity sha512-oupYfh0lTHg+F/2ZoTNrioB+KLd6x0Zlhjz2Oa1jhl8wCGkNvwe25RytR2/SGPYpoNVcvCeoayWQRwwRuWGgfQ== + +esbuild-linux-arm@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.7.tgz#3d665b35e1c27dbe1c9deb8bf956d7d1f191a21b" + integrity sha512-OzpXEBogbYdcBqE4uKynuSn5YSetCvK03Qv1HcOY1VN6HmReuatjJ21dCH+YPHSpMEF0afVCnNfffvsGEkxGJQ== + +esbuild-linux-arm@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.17.tgz#05470e182cfaa4677090a54db96253a2a319e5dc" + integrity sha512-+ugCmBTTDIlh+UuC7E/GvyJqjGTX2pNOA+g3isG78aYcfgswrHjvstTtIfljaU95AS30qrVNLgI5h/8TsRWTrg== + +esbuild-linux-mips64le@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.7.tgz#226114a0cc6649ba0ffd3428118a8f622872f16d" + integrity sha512-mFWpnDhZJmj/h7pxqn1GGDsKwRfqtV7fx6kTF5pr4PfXe8pIaTERpwcKkoCwZUkWAOmUEjMIUAvFM72A6hMZnA== + +esbuild-linux-mips64le@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.17.tgz#0e806ef7e0b7148fe9944d736e7e72c40f2bfec0" + integrity sha512-aUVyHwUXJF1hi9jsAT+At+cBxZh2yGICi/e757N6d/zzOD+eVK3PKQj68tAvIflx6/ZpnuCTKol1GpgGYrzERg== + +esbuild-linux-ppc64le@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.7.tgz#5c67ae56517f2644d567b2ca5ecb97f9520cfc49" + integrity sha512-wM7f4M0bsQXfDL4JbbYD0wsr8cC8KaQ3RPWc/fV27KdErPW7YsqshZZSjDV0kbhzwpNNdhLItfbaRT8OE8OaKA== + +esbuild-linux-ppc64le@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.17.tgz#9c2b392efd372ddc2ac4e7d2cfd1d2b993a3031d" + integrity sha512-i7789iFTLfLccHPNADCbaZPx9CuQblsBqv2j4XqIBN1jKIJbpQ8iqCkWoHep4PLqqKLtBLtTWh919GsrFGdeJA== + +esbuild-linux-riscv64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.17.tgz#ac803768edebef91070891753565bf9ce25ece77" + integrity sha512-fEQ/8tnZ2sDniBlPfTXEdg+0OP1olps96HvYdwl8ywJdAlD7AK761EL3lRbRdfMHNOId2N6+CVca43/Fiu/0AQ== + +esbuild-linux-s390x@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.17.tgz#ac70e5326c16ef8b2b89d72b54c44c0c99d15401" + integrity sha512-ZBQekST4gYgTKHAvUJtR1kFFulHTDlRZSE8T0wRQCmQqydNkC1teWxlR31xS6MZevjZGfa7OMVJD24bBhei/2Q== + +esbuild-netbsd-64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.7.tgz#69dc0469ea089013956d8c6aa71c9e7fc25fc567" + integrity sha512-J/afS7woKyzGgAL5FlgvMyqgt5wQ597lgsT+xc2yJ9/7BIyezeXutXqfh05vszy2k3kSvhLesugsxIA71WsqBw== + +esbuild-netbsd-64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.17.tgz#14eec8ded46e1d7ca564c81cf0ecd3cfbdeb89db" + integrity sha512-onNBFaZVN9GzGJMm3aZJJv74n/Q8FjW20G9OfSDhHjvamqJ5vbd42hNk6igQX4lgBCHTZvvBlWDJAMy+tbJAAw== + +esbuild-openbsd-64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.7.tgz#3a9d04ecf820708e2e5b7d26fa7332e3f19f6b6c" + integrity sha512-7CcxgdlCD+zAPyveKoznbgr3i0Wnh0L8BDGRCjE/5UGkm5P/NQko51tuIDaYof8zbmXjjl0OIt9lSo4W7I8mrw== + +esbuild-openbsd-64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.17.tgz#363e1a96e402ad1a8a35bf8a3f7b64b8703fa1de" + integrity sha512-QFxHmvjaRrmTCvH/A3EmzqKUSZHRQ7/pbrJeATsb/Q6qckCeL9e7zg/1A3HiZqDXeBUV3yNeBeV1GJBjY6yVyA== + +esbuild-plugin-external-global@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esbuild-plugin-external-global/-/esbuild-plugin-external-global-1.0.1.tgz#e3bba0e3a561f61b395bec0984a90ed0de06c4ce" + integrity sha512-NDzYHRoShpvLqNcrgV8ZQh61sMIFAry5KLTQV83BPG5iTXCCu7h72SCfJ97bW0GqtuqDD/1aqLbKinI/rNgUsg== + +esbuild-sass-plugin@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/esbuild-sass-plugin/-/esbuild-sass-plugin-2.4.3.tgz#0eeca5bcf08606324d82ecf2556ef98f64ce0c71" + integrity sha512-iEfikpoZGoUd46K2nJ195n+4/75n8xYFL5/LDAaaRkXRxzZuZ9AZS4EdW7hTYQwEMkUiBIASHHouuU8Ioovv9Q== + dependencies: + esbuild "^0.15.17" + resolve "^1.22.1" + sass "^1.56.1" + +esbuild-sunos-64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.7.tgz#7c33a682f0fd9565cae7df165d0e8736b7b62623" + integrity sha512-GKCafP2j/KUljVC3nesw1wLFSZktb2FGCmoT1+730zIF5O6hNroo0bSEofm6ZK5mNPnLiSaiLyRB9YFgtkd5Xg== + +esbuild-sunos-64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.17.tgz#0d6def4f946f4b585845373010a8d35310356ed0" + integrity sha512-7dHZA8Kc6U8rBTKojJatXtzHTUKJ3CRYimvOGIQQ1yUDOqGx/zZkCH/HkEi3Zg5SWyDj/57E5e1YJPo4ySSw/w== + +esbuild-windows-32@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.7.tgz#24ec706a5f25b4499048f56146bcff0ed3839dce" + integrity sha512-5I1GeL/gZoUUdTPA0ws54bpYdtyeA2t6MNISalsHpY269zK8Jia/AXB3ta/KcDHv2SvNwabpImeIPXC/k0YW6A== + +esbuild-windows-32@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.17.tgz#3e5260f46c6a1cb0fafcb4c7c5aa1371ca0f525d" + integrity sha512-yDrNrwQ/0k4N3OZItZ6k6YnBUch8+of06YRYc3hFI8VDm7X1rkNZwhttZNAzF6+TtbnK4cIz7H2/EwdSoaGZ3g== + +esbuild-windows-64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.7.tgz#dd6d5b5bace93cd7a9174d31fbd727ba21885abf" + integrity sha512-CIGKCFpQOSlYsLMbxt8JjxxvVw9MlF1Rz2ABLVfFyHUF5OeqHD5fPhGrCVNaVrhO8Xrm+yFmtjcZudUGr5/WYQ== + +esbuild-windows-64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.17.tgz#d313f92c3dcc191d14d5d06a6722ae250c158aeb" + integrity sha512-jPnXvB4zMMToNPpCBdt+OEQiYFVs9wlQ5G8vMoJkrYJBp1aEt070MRpBFa6pfBFrgXquqgUiNAohMcTdy+JVFg== + +esbuild-windows-arm64@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.7.tgz#ecfd9ac289606f26760c4f737caaeeadfff3cfe3" + integrity sha512-eOs1eSivOqN7cFiRIukEruWhaCf75V0N8P0zP7dh44LIhLl8y6/z++vv9qQVbkBm5/D7M7LfCfCTmt1f1wHOCw== + +esbuild-windows-arm64@0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.17.tgz#7ad50ae269a490c3b12c3128fb67d91ec2caf869" + integrity sha512-I5QeSsz0X66V8rxVhmw03Wzn8Tz63H3L9GrsA7C5wvBXMk3qahLWuEL+l7SZ2DleKkFeZZMu1dPxOak9f1TZ4A== + +esbuild@0.14.7: + version "0.14.7" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.7.tgz#e85cead55b0e1001abf1b2ce4a11c1d4d709d13c" + integrity sha512-+u/msd6iu+HvfysUPkZ9VHm83LImmSNnecYPfFI01pQ7TTcsFR+V0BkybZX7mPtIaI7LCrse6YRj+v3eraJSgw== + optionalDependencies: + esbuild-android-arm64 "0.14.7" + esbuild-darwin-64 "0.14.7" + esbuild-darwin-arm64 "0.14.7" + esbuild-freebsd-64 "0.14.7" + esbuild-freebsd-arm64 "0.14.7" + esbuild-linux-32 "0.14.7" + esbuild-linux-64 "0.14.7" + esbuild-linux-arm "0.14.7" + esbuild-linux-arm64 "0.14.7" + esbuild-linux-mips64le "0.14.7" + esbuild-linux-ppc64le "0.14.7" + esbuild-netbsd-64 "0.14.7" + esbuild-openbsd-64 "0.14.7" + esbuild-sunos-64 "0.14.7" + esbuild-windows-32 "0.14.7" + esbuild-windows-64 "0.14.7" + esbuild-windows-arm64 "0.14.7" + +esbuild@^0.15.1, esbuild@^0.15.17: + version "0.15.17" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.17.tgz#9a4e1e78898968afbdce4325e3b941cb7378ee42" + integrity sha512-8MbkDX+kh0kaeYGd6klMbn1uTOXHoDw7UYMd1dQYA5cqBZivf5+pzfaXZSL1RNamJfXW/uWC5+9wX5ejDgpSqg== + optionalDependencies: + "@esbuild/android-arm" "0.15.17" + "@esbuild/linux-loong64" "0.15.17" + esbuild-android-64 "0.15.17" + esbuild-android-arm64 "0.15.17" + esbuild-darwin-64 "0.15.17" + esbuild-darwin-arm64 "0.15.17" + esbuild-freebsd-64 "0.15.17" + esbuild-freebsd-arm64 "0.15.17" + esbuild-linux-32 "0.15.17" + esbuild-linux-64 "0.15.17" + esbuild-linux-arm "0.15.17" + esbuild-linux-arm64 "0.15.17" + esbuild-linux-mips64le "0.15.17" + esbuild-linux-ppc64le "0.15.17" + esbuild-linux-riscv64 "0.15.17" + esbuild-linux-s390x "0.15.17" + esbuild-netbsd-64 "0.15.17" + esbuild-openbsd-64 "0.15.17" + esbuild-sunos-64 "0.15.17" + esbuild-windows-32 "0.15.17" + esbuild-windows-64 "0.15.17" + esbuild-windows-arm64 "0.15.17" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + +escape-html@^1.0.3, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +eslint-scope@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.29.0: + version "8.29.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.29.0.tgz#d74a88a20fb44d59c51851625bc4ee8d0ec43f87" + integrity sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg== + dependencies: + "@eslint/eslintrc" "^1.3.3" + "@humanwhocodes/config-array" "^0.11.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.15.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + +espree@^9.4.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd" + integrity sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.1.0, esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-stream@3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" + integrity sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g== + dependencies: + duplexer "~0.1.1" + from "~0" + map-stream "~0.1.0" + pause-stream "0.0.11" + split "0.3" + stream-combiner "~0.0.4" + through "~2.3.1" + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== + +events@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +eventsource@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-2.0.2.tgz#76dfcc02930fb2ff339520b6d290da573a9e8508" + integrity sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA== + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA== + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +express@^4.17.1: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q== + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^2.2.6: + version "2.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" + integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== + dependencies: + "@mrmlnc/readdir-enhanced" "^2.2.1" + "@nodelib/fs.stat" "^1.1.2" + glob-parent "^3.1.0" + is-glob "^4.0.0" + merge2 "^1.2.3" + micromatch "^3.1.10" + +fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.9: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastest-levenshtein@^1.0.16: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +faye-websocket@0.11.x, faye-websocket@^0.11.3, faye-websocket@^0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +figgy-pudding@^3.5.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" + integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +file-loader@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" + integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw== + dependencies: + loader-utils "^1.0.2" + schema-utils "^1.0.0" + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ== + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@5.0.0, find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ== + dependencies: + locate-path "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== + +flush-write-stream@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" + integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== + dependencies: + inherits "^2.0.3" + readable-stream "^2.3.6" + +follow-redirects@^1.0.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== + +foreach@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.6.tgz#87bcc8a1a0e74000ff2bf9802110708cfb02eb6e" + integrity sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg== + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +format-thousands@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/format-thousands/-/format-thousands-1.1.1.tgz#7975bee30338d9006390da5831db0b41c323fbfa" + integrity sha512-OrNyhmdvoDfpcUVU3N4SqjfLTfnYVmc8JFiUo3Ar1IwxLv/QJC9PpU5dYuLrHTYzpLC8hF9aCBsfmgaoHlWJMQ== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA== + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g== + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +from@~0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + integrity sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g== + +fs-extra@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + integrity sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA== + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^1.2.7: + version "1.2.13" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" + integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== + dependencies: + bindings "^1.5.0" + nan "^2.12.1" + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-stream@^4.0.0, get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA== + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + +git-hooks-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-3.0.0.tgz#6d888988bb445b34e7c2e1eb97cb88358153221e" + integrity sha512-XDfdemBGJIMAsHHOONHQxEH5dX2kCpE6MGZ1IsNvBuDPBZM3p4EAwAC7ygMjn/1/x+BJX0TK1ara1Zrh7JCFdQ== + +github-buttons@^2.8.0: + version "2.22.1" + resolved "https://registry.yarnpkg.com/github-buttons/-/github-buttons-2.22.1.tgz#1221aef6dee885665c81b9b3e913471951790ec1" + integrity sha512-937dpW009lbV8p1gg1SoUvUnJBnfJKYU6CEJcsdoLnBzI4YP7Y8oT/M0O7WTQwfErTYLCG9t0W1huaexSLx7yA== + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA== + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-promise@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-3.4.0.tgz#b6b8f084504216f702dc2ce8c9bc9ac8866fdb20" + integrity sha512-q08RJ6O+eJn+dVanerAndJwIcumgbDdYiUT7zFQl3Wm1xD6fBKtah7H8ZJChj4wP+8C+QfeVy8xautR7rdmKEw== + dependencies: + "@types/glob" "*" + +glob-to-regexp@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + integrity sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig== + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d" + integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ== + dependencies: + ini "1.3.7" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +global@^4.3.2: + version "4.4.0" + resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" + integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w== + dependencies: + min-document "^2.19.0" + process "^0.11.10" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.15.0: + version "13.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.18.0.tgz#fb224daeeb2bb7d254cd2c640f003528b8d0c1dc" + integrity sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A== + dependencies: + type-fest "^0.20.2" + +globby@^11.0.3, globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globby@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.2.tgz#29047105582427ab6eca4f905200667b056da515" + integrity sha512-LKSDZXToac40u8Q1PQtZihbNdTYSNMuWe+K5l+oa6KgDzSvVrHXlJy40hUP522RjAIoNLJYBJi7ow+rbFpIhHQ== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.2.11" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^4.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw== + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globby@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" + integrity sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g== + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + +globby@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" + integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg== + dependencies: + "@types/glob" "^7.1.1" + array-union "^1.0.2" + dir-glob "^2.2.2" + fast-glob "^2.2.6" + glob "^7.1.3" + ignore "^4.0.3" + pify "^4.0.1" + slash "^2.0.0" + +globjoin@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" + integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg== + +globs@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/globs/-/globs-0.1.4.tgz#1d13639f6174e4ae73a7f936da7d9a079f657c1c" + integrity sha512-D23dWbOq48vlOraoSigbcQV4tWrnhwk+E/Um2cMuDS3/5dwGmdFeA7L/vAvDhLFlQOTDqHcXh35m/71g2A2WzQ== + dependencies: + glob "^7.1.1" + +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +gray-matter@^4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" + integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== + dependencies: + js-yaml "^3.13.1" + kind-of "^6.0.2" + section-matter "^1.0.0" + strip-bom-string "^1.0.0" + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +hard-rejection@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + +has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q== + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw== + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ== + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ== + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + +has@^1.0.0, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash-sum@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04" + integrity sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA== + +hash-sum@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a" + integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg== + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +he@1.2.0, he@1.2.x, he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hex-color-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" + integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== + +highlight.js@^9.7.0: + version "9.18.5" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" + integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA== + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hogan.js@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/hogan.js/-/hogan.js-3.0.2.tgz#4cd9e1abd4294146e7679e41d7898732b02c7bfd" + integrity sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg== + dependencies: + mkdirp "0.3.0" + nopt "1.0.10" + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +hosted-git-info@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== + dependencies: + lru-cache "^6.0.0" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +hsl-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" + integrity sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A== + +hsla-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" + integrity sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA== + +html-entities@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc" + integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA== + +html-minifier@^3.2.3: + version "3.5.21" + resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-3.5.21.tgz#d0040e054730e354db008463593194015212d20c" + integrity sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA== + dependencies: + camel-case "3.0.x" + clean-css "4.2.x" + commander "2.17.x" + he "1.2.x" + param-case "2.1.x" + relateurl "0.2.x" + uglify-js "3.4.x" + +html-tags@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-2.0.0.tgz#10b30a386085f43cede353cc8fa7cb0deeea668b" + integrity sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g== + +html-tags@^3.1.0, html-tags@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961" + integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg== + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + +http-auth@3.1.x: + version "3.1.3" + resolved "https://registry.yarnpkg.com/http-auth/-/http-auth-3.1.3.tgz#945cfadd66521eaf8f7c84913d377d7b15f24e31" + integrity sha512-Jbx0+ejo2IOx+cRUYAGS1z6RGc6JfYUNkysZM4u4Sfk1uLlGv814F7/PIjQQAuThLdAWxb74JMGd5J8zex1VQg== + dependencies: + apache-crypt "^1.1.2" + apache-md5 "^1.0.6" + bcryptjs "^2.3.0" + uuid "^3.0.0" + +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-middleware@0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== + dependencies: + http-proxy "^1.17.0" + is-glob "^4.0.0" + lodash "^4.17.11" + micromatch "^3.1.10" + +http-proxy-middleware@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz#43700d6d9eecb7419bf086a128d0f7205d9eb665" + integrity sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg== + dependencies: + "@types/http-proxy" "^1.17.5" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.17.0, http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + integrity sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg== + +icss-utils@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467" + integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA== + dependencies: + postcss "^7.0.14" + +ieee754@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + integrity sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA== + +ignore@^3.3.5: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== + +ignore@^4.0.3: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +ignore@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.1.tgz#c2b1f76cb999ede1502f3a226a9310fdfe88d46c" + integrity sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA== + +immediate@^3.2.3: + version "3.3.0" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" + integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== + +immutable@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef" + integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ== + +import-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" + integrity sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg== + dependencies: + import-from "^2.1.0" + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg== + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-from@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" + integrity sha512-0vdnLL2wSGnhlRmzHJAg5JHjt1l2vYhzJ7tNLGbeVg0fse56tpGaH0uzH+r9Slej+BSXXEHvBKDEnVSLLE9/+w== + dependencies: + resolve-from "^3.0.0" + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A== + +import-lazy@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" + integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw== + +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + integrity sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + integrity sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA== + +infer-owner@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +ini@1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== + +ini@^1.3.5, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +internal-ip@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" + integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== + dependencies: + default-gateway "^4.2.0" + ipaddr.js "^1.9.0" + +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw== + +ip@^1.1.0, ip@^1.1.5: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.8.tgz#ae05948f6b075435ed3307acce04629da8cdbf48" + integrity sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg== + +ipaddr.js@1.9.1, ipaddr.js@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + integrity sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg== + +is-absolute-url@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" + integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A== + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q== + dependencies: + binary-extensions "^1.0.0" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-color-stop@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" + integrity sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA== + dependencies: + css-color-names "^0.0.4" + hex-color-regex "^1.1.0" + hsl-regex "^1.0.0" + hsla-regex "^1.0.0" + rgb-regex "^1.0.1" + rgba-regex "^1.0.0" + +is-core-module@^2.5.0, is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg== + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw== + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw== + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" + integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== + dependencies: + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-npm@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" + integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg== + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-cwd@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-path-inside@^3.0.1, is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-obj@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +is-regex@^1.0.4, is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-resolvable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" + integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typedarray@^1.0.0, is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isarray@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA== + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + +istextorbinary@^2.2.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.6.0.tgz#60776315fb0fa3999add276c02c69557b9ca28ab" + integrity sha512-+XRlFseT8B3L9KyjxxLjfXSLMuErKDsd8DBNrsaxoViABMEZlOSCstwmw0qpoFX3+U6yWU1yhLudAe6/lETGGA== + dependencies: + binaryextensions "^2.1.2" + editions "^2.2.0" + textextensions "^2.5.0" + +javascript-stringify@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3" + integrity sha512-fnjC0up+0SjEJtgmmG+teeel68kutkvzfctO/KxE3qJlbunkJYAshgH3boU++gSBHP8z5/r0ts0qRIrHf0RTQQ== + +javascript-stringify@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79" + integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg== + +joycon@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" + integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== + +js-sdsl@^4.1.4: + version "4.2.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" + integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== + +json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + integrity sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +json5@^2.1.2, json5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +jsonc-parser@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + +killable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ== + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw== + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +known-css-properties@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.26.0.tgz#008295115abddc045a9f4ed7e2a84dc8b3a77649" + integrity sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg== + +last-call-webpack-plugin@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555" + integrity sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w== + dependencies: + lodash "^4.17.5" + webpack-sources "^1.1.0" + +latest-version@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lilconfig@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" + integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +linkify-it@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" + integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw== + dependencies: + uc.micro "^1.0.1" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw== + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +load-script@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/load-script/-/load-script-1.0.0.tgz#0491939e0bee5643ee494a7e3da3d2bac70c6ca4" + integrity sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA== + +load-tsconfig@^0.2.0: + version "0.2.3" + resolved "https://registry.yarnpkg.com/load-tsconfig/-/load-tsconfig-0.2.3.tgz#08af3e7744943caab0c75f8af7f1703639c3ef1f" + integrity sha512-iyT2MXws+dc2Wi6o3grCFtGXpeMvHmJqS27sMPGtV2eUu4PeFnG+33I8BlFK1t1NWMjOpcx9bridn5yxLDX2gQ== + +loader-runner@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" + integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== + +loader-utils@^0.2.16: + version "0.2.17" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" + integrity sha512-tiv66G0SmiOx+pLWMtGEkfSEejxvb6N6uRrQjfWJIT79W9GMpgKeCAmm9aVBKtd4WEgntciI8CsGqjpDoCWJug== + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + object-assign "^4.0.1" + +loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3: + version "1.4.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" + integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +loader-utils@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA== + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA== + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.kebabcase@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36" + integrity sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g== + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + +lodash.template@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + +lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.5: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +loglevel-plugin-prefix@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz#2fe0e05f1a820317d98d8c123e634c1bd84ff644" + integrity sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g== + +loglevel@^1.6.6, loglevel@^1.6.8: + version "1.8.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4" + integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg== + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ== + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lower-case@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + integrity sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA== + +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@^4.1.2: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lunr@^2.3.9: + version "2.3.9" + resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" + integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ== + +make-dir@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg== + +map-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== + +map-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" + integrity sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ== + +map-obj@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" + integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== + +map-stream@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" + integrity sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g== + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w== + dependencies: + object-visit "^1.0.0" + +markdown-it-anchor@^5.0.2: + version "5.3.0" + resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz#d549acd64856a8ecd1bea58365ef385effbac744" + integrity sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA== + +markdown-it-chain@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/markdown-it-chain/-/markdown-it-chain-1.3.0.tgz#ccf6fe86c10266bafb4e547380dfd7f277cc17bc" + integrity sha512-XClV8I1TKy8L2qsT9iX3qiV+50ZtcInGXI80CA+DP62sMs7hXlyV/RM3hfwy5O3Ad0sJm9xIwQELgANfESo8mQ== + dependencies: + webpack-chain "^4.9.0" + +markdown-it-container@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-2.0.0.tgz#0019b43fd02eefece2f1960a2895fba81a404695" + integrity sha512-IxPOaq2LzrGuFGyYq80zaorXReh2ZHGFOB1/Hen429EJL1XkPI3FJTpx9TsJeua+j2qTru4h3W1TiCRdeivMmA== + +markdown-it-emoji@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz#9bee0e9a990a963ba96df6980c4fddb05dfb4dcc" + integrity sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg== + +markdown-it-table-of-contents@^0.4.0: + version "0.4.4" + resolved "https://registry.yarnpkg.com/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz#3dc7ce8b8fc17e5981c77cc398d1782319f37fbc" + integrity sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw== + +markdown-it@^8.4.1: + version "8.4.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54" + integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ== + dependencies: + argparse "^1.0.7" + entities "~1.1.1" + linkify-it "^2.0.0" + mdurl "^1.0.1" + uc.micro "^1.0.5" + +marked@^4.0.19, marked@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.3.tgz#bd76a5eb510ff1d8421bc6c3b2f0b93488c15bea" + integrity sha512-slWRdJkbTZ+PjkyJnE30Uid64eHwbwa1Q25INCAYfZlK4o6ylagBy/Le9eWntqJFoFT93ikUKMv47GZ4gTwHkw== + +mathml-tag-names@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" + integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memory-fs@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + integrity sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +meow@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" + integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig== + dependencies: + camelcase-keys "^4.0.0" + decamelize-keys "^1.0.0" + loud-rejection "^1.0.0" + minimist-options "^3.0.1" + normalize-package-data "^2.3.4" + read-pkg-up "^3.0.0" + redent "^2.0.0" + trim-newlines "^2.0.0" + yargs-parser "^10.0.0" + +meow@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" + integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize "^1.2.0" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.18.0" + yargs-parser "^20.2.3" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== + dependencies: + source-map "^0.6.1" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.0.3, mime@^2.4.4: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +min-document@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" + integrity sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ== + dependencies: + dom-walk "^0.1.0" + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +mini-css-extract-plugin@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.6.0.tgz#a3f13372d6fcde912f3ee4cd039665704801e3b9" + integrity sha512-79q5P7YGI6rdnVyIAV4NXpBQJFWdkzJxCim3Kog4078fM0piAaFlwocqbejdWtLW1cEzCexPrh6EdyFsPgVdAw== + dependencies: + loader-utils "^1.1.0" + normalize-url "^2.0.1" + schema-utils "^1.0.0" + webpack-sources "^1.1.0" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" + integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== + dependencies: + brace-expansion "^2.0.1" + +minimist-options@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" + integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + kind-of "^6.0.3" + +minimist-options@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" + integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +mississippi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" + integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^3.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" + integrity sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew== + +mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.6, mkdirp@~0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mocha@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.1.0.tgz#dbf1114b7c3f9d0ca5de3133906aea3dfc89ef7a" + integrity sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +morgan@^1.9.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + integrity sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ== + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ== + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nan@^2.12.1: + version "2.17.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" + integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== + +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + +nanoid@^3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.5.0, neo-async@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +no-case@^2.2.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" + integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== + dependencies: + lower-case "^1.1.1" + +node-fetch@^2.3.0: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== + +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^3.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.1" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.11.0" + vm-browserify "^1.0.1" + +node-releases@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + +nopt@1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" + integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== + dependencies: + abbrev "1" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-package-data@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" + integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== + dependencies: + hosted-git-info "^4.0.1" + is-core-module "^2.5.0" + semver "^7.3.4" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w== + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + +normalize-url@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" + integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== + dependencies: + prepend-http "^2.0.0" + query-string "^5.0.1" + sort-keys "^2.0.0" + +normalize-url@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" + integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== + +normalize-url@^4.1.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw== + dependencies: + path-key "^2.0.0" + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nprogress@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1" + integrity sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA== + +nth-check@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1, object-assign@latest: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ== + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-inspect@^1.12.2, object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-is@^1.0.1: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +object-keys@^1.0.11, object-keys@^1.1.0, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA== + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0, object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: + version "2.1.5" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz#db5a9002489b64eef903df81d6623c07e5b4b4d3" + integrity sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw== + dependencies: + array.prototype.reduce "^1.0.5" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ== + dependencies: + isobject "^3.0.1" + +object.values@^1.1.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" + integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@latest: + version "8.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8" + integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +opencollective-postinstall@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" + integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== + +opn@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + +optimize-css-assets-webpack-plugin@^5.0.1: + version "5.0.8" + resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.8.tgz#cbccdcf5a6ef61d4f8cc78cf083a67446e5f402a" + integrity sha512-mgFS1JdOtEGzD8l+EuISqL57cKO+We9GcoiQEmdCWRqqck+FGNmYJtx9qfAPzEz+lRrlThWMuGDaRkI/yWNx/Q== + dependencies: + cssnano "^4.1.10" + last-call-webpack-plugin "^3.0.0" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== + +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.2.1: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg== + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-retry@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" + integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== + dependencies: + retry "^0.12.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww== + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + +pako@~1.0.5: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +parallel-transform@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc" + integrity sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg== + dependencies: + cyclist "^1.0.1" + inherits "^2.0.3" + readable-stream "^2.1.5" + +param-case@2.1.x: + version "2.1.1" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" + integrity sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w== + dependencies: + no-case "^2.2.0" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-asn1@^5.0.0, parse-asn1@^5.1.5: + version "5.1.6" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" + integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== + dependencies: + asn1.js "^5.2.0" + browserify-aes "^1.0.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw== + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== + +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pause-stream@0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + integrity sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A== + dependencies: + through "~2.3" + +pbkdf2@^3.0.3: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" + integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== + +pirates@^4.0.1: + version "4.0.5" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +portfinder@^1.0.13, portfinder@^1.0.26: + version "1.0.32" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.32.tgz#2fe1b9e58389712429dc2bea5beb2146146c7f81" + integrity sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg== + dependencies: + async "^2.6.4" + debug "^3.2.7" + mkdirp "^0.5.6" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg== + +postcss-calc@^7.0.1: + version "7.0.5" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e" + integrity sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg== + dependencies: + postcss "^7.0.27" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.0.2" + +postcss-colormin@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" + integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== + dependencies: + browserslist "^4.0.0" + color "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-convert-values@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" + integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-discard-comments@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" + integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== + dependencies: + postcss "^7.0.0" + +postcss-discard-duplicates@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" + integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== + dependencies: + postcss "^7.0.0" + +postcss-discard-empty@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" + integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== + dependencies: + postcss "^7.0.0" + +postcss-discard-overridden@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" + integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== + dependencies: + postcss "^7.0.0" + +postcss-load-config@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.2.tgz#c5ea504f2c4aef33c7359a34de3573772ad7502a" + integrity sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw== + dependencies: + cosmiconfig "^5.0.0" + import-cwd "^2.0.0" + +postcss-load-config@^3.0.1: + version "3.1.4" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" + integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== + dependencies: + lilconfig "^2.0.5" + yaml "^1.10.2" + +postcss-loader@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" + integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== + dependencies: + loader-utils "^1.1.0" + postcss "^7.0.0" + postcss-load-config "^2.0.0" + schema-utils "^1.0.0" + +postcss-media-query-parser@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" + integrity sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig== + +postcss-merge-longhand@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" + integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== + dependencies: + css-color-names "0.0.4" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + stylehacks "^4.0.0" + +postcss-merge-rules@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" + integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + cssnano-util-same-parent "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + vendors "^1.0.0" + +postcss-minify-font-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" + integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-gradients@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" + integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + is-color-stop "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-minify-params@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" + integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== + dependencies: + alphanum-sort "^1.0.0" + browserslist "^4.0.0" + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + uniqs "^2.0.0" + +postcss-minify-selectors@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" + integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== + dependencies: + alphanum-sort "^1.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +postcss-modules-extract-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e" + integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ== + dependencies: + postcss "^7.0.5" + +postcss-modules-local-by-default@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63" + integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + postcss-value-parser "^3.3.1" + +postcss-modules-scope@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee" + integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ== + dependencies: + postcss "^7.0.6" + postcss-selector-parser "^6.0.0" + +postcss-modules-values@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64" + integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w== + dependencies: + icss-replace-symbols "^1.1.0" + postcss "^7.0.6" + +postcss-normalize-charset@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" + integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== + dependencies: + postcss "^7.0.0" + +postcss-normalize-display-values@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" + integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-positions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" + integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== + dependencies: + cssnano-util-get-arguments "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-repeat-style@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" + integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== + dependencies: + cssnano-util-get-arguments "^4.0.0" + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-string@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" + integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== + dependencies: + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-timing-functions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" + integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== + dependencies: + cssnano-util-get-match "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-unicode@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" + integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-url@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" + integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^3.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-normalize-whitespace@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" + integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-ordered-values@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" + integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== + dependencies: + cssnano-util-get-arguments "^4.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-reduce-initial@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" + integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== + dependencies: + browserslist "^4.0.0" + caniuse-api "^3.0.0" + has "^1.0.0" + postcss "^7.0.0" + +postcss-reduce-transforms@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" + integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== + dependencies: + cssnano-util-get-match "^4.0.0" + has "^1.0.0" + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + +postcss-resolve-nested-selector@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz#29ccbc7c37dedfac304e9fff0bf1596b3f6a0e4e" + integrity sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw== + +postcss-safe-parser@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz#a6d4e48f0f37d9f7c11b2a581bf00f8ba4870b96" + integrity sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g== + dependencies: + postcss "^7.0.26" + +postcss-safe-parser@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" + integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== + +postcss-scss@^4.0.2: + version "4.0.5" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.5.tgz#8ee33c1dda8d9d4753b565ec79014803dc6edabf" + integrity sha512-F7xpB6TrXyqUh3GKdyB4Gkp3QL3DDW1+uI+gxx/oJnUt/qXI4trj5OGlp9rOKdoABGULuqtqeG+3HEVQk4DjmA== + +postcss-selector-parser@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270" + integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA== + dependencies: + dot-prop "^5.2.0" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.6: + version "6.0.11" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" + integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-svgo@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e" + integrity sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw== + dependencies: + postcss "^7.0.0" + postcss-value-parser "^3.0.0" + svgo "^1.0.0" + +postcss-unique-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" + integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== + dependencies: + alphanum-sort "^1.0.0" + postcss "^7.0.0" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.0, postcss-value-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + +postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.36, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== + dependencies: + picocolors "^0.2.1" + source-map "^0.6.1" + +postcss@^8.4.14, postcss@^8.4.19: + version "8.4.19" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.19.tgz#61178e2add236b17351897c8bcc0b4c8ecab56fc" + integrity sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== + +"prettier@^1.18.2 || ^2.0.0", prettier@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.0.tgz#c7df58393c9ba77d6fba3921ae01faf994fb9dc9" + integrity sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA== + +pretty-bytes@^5.3.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +pretty-error@^2.0.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" + integrity sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw== + dependencies: + lodash "^4.17.20" + renderkid "^2.0.4" + +pretty-time@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" + integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== + +prismjs@^1.13.0: + version "1.29.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12" + integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-middleware@latest: + version "0.15.0" + resolved "https://registry.yarnpkg.com/proxy-middleware/-/proxy-middleware-0.15.0.tgz#a3fdf1befb730f951965872ac2f6074c61477a56" + integrity sha512-EGCG8SeoIRVMhsqHQUdDigB2i7qU7fCsWASwn54+nPutYO8n4q6EiwMzyfWlC+dzRFExP+kvcnDFdBDHoZBU7Q== + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== + +psl@^1.1.28: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw== + +punycode@^1.2.4: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +pupa@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" + integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== + dependencies: + escape-goat "^2.0.0" + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + +query-string@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0, querystring-es3@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + integrity sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA== + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g== + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quick-lru@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" + integrity sha512-tRS7sTgyxMXtLum8L65daJnHUhfDUgboRdcWW2bR9vBfrj2+O5HSMbQOJfJJjIVSPFqbBCF37FpwWXGitDc5tA== + +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-loader@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" + integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +rc@1.2.8, rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-pkg-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" + integrity sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw== + dependencies: + find-up "^2.0.0" + read-pkg "^3.0.0" + +read-pkg-up@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-6.0.0.tgz#da75ce72762f2fa1f20c5a40d4dd80c77db969e3" + integrity sha512-odtTvLl+EXo1eTsMnoUHRmg/XmXdTkwXVxy4VFE9Kp6cCq7b3l7QMdBndND3eAFzrbSAXC/WCUOQQ9rLjifKZw== + dependencies: + find-up "^4.0.0" + read-pkg "^5.1.1" + type-fest "^0.5.0" + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA== + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +read-pkg@^5.1.1, read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +redent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" + integrity sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw== + dependencies: + indent-string "^3.0.0" + strip-indent "^2.0.0" + +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + +reduce@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/reduce/-/reduce-1.0.2.tgz#0cd680ad3ffe0b060e57a5c68bdfce37168d361b" + integrity sha512-xX7Fxke/oHO5IfZSk77lvPa/7bjMh9BuCk4OOoX5XTXrM7s0Z+MkPfSDfz0q7r91BhhGSs8gii/VEN/7zhCPpQ== + dependencies: + object-keys "^1.1.0" + +regenerate-unicode-properties@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" + integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.10: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-transform@^0.15.0: + version "0.15.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" + integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== + dependencies: + "@babel/runtime" "^7.8.4" + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +regexpu-core@^5.1.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.2.2.tgz#3e4e5d12103b64748711c3aad69934d7718e75fc" + integrity sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsgen "^0.7.1" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +registry-auth-token@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.2.tgz#f02d49c3668884612ca031419491a13539e21fac" + integrity sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg== + dependencies: + rc "1.2.8" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + +regjsgen@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.7.1.tgz#ee5ef30e18d3f09b7c369b76e7c2373ed25546f6" + integrity sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA== + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +relateurl@0.2.x: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw== + +renderkid@^2.0.4: + version "2.0.7" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-2.0.7.tgz#464f276a6bdcee606f4a15993f9b29fc74ca8609" + integrity sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ== + dependencies: + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^3.0.1" + +repeat-element@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.4.tgz#be681520847ab58c7568ac75fbfad28ed42d39e9" + integrity sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + +request@^2.87.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg== + dependencies: + resolve-from "^3.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== + +resolve@^1.10.0, resolve@^1.14.2, resolve@^1.22.0, resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ== + dependencies: + lowercase-keys "^1.0.0" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rgb-regex@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" + integrity sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w== + +rgba-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" + integrity sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg== + +rimraf@^2.5.4, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +rollup@^3.2.5: + version "3.4.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.4.0.tgz#3f363d46474deb54e6da38d398c3af845c1b7d43" + integrity sha512-4g8ZrEFK7UbDvy3JF+d5bLiC8UKkS3n/27/cnVeESwB1LVPl6MoPL32/6+SCQ1vHTp6Mvp2veIHtwELhi+uXEw== + optionalDependencies: + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + integrity sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg== + dependencies: + aproba "^1.1.1" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg== + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@^2.1.2, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sass@^1.23.7, sass@^1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.1.tgz#94d3910cd468fd075fa87f5bb17437a0b617d8a7" + integrity sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +sax@~1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +scss-bundle@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/scss-bundle/-/scss-bundle-3.1.2.tgz#8919dd7603d01a84822e8aab5210e5b0b50c548b" + integrity sha512-lvxTwCKDLgzmRWhGwJ834ggtnEhs0G9FxSJRWte+NwlshVvBcQ/kOHHkpAGDpCxIMNGz/Utl0yd/MWyQAOBhqg== + dependencies: + "@types/archy" "^0.0.31" + "@types/debug" "^4.1.5" + "@types/fs-extra" "^8.0.1" + "@types/glob" "^7.1.1" + "@types/lodash.debounce" "^4.0.6" + "@types/sass" "^1.16.0" + archy "^1.0.0" + chalk "^3.0.0" + chokidar "^3.3.1" + commander "^4.0.1" + fs-extra "^8.1.0" + globs "^0.1.4" + lodash.debounce "^4.0.8" + loglevel "^1.6.6" + loglevel-plugin-prefix "^0.8.4" + pretty-bytes "^5.3.0" + sass "^1.23.7" + tslib "^1.10.0" + +section-matter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" + integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== + dependencies: + extend-shallow "^2.0.1" + kind-of "^6.0.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^1.10.8: + version "1.10.14" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.14.tgz#ee51d84d9dcecc61e07e4aba34f229ab525c1574" + integrity sha512-lkjaiAye+wBZDCBsu5BGi0XiLRxeUlsGod5ZP924CRSEoGuZAw/f7y9RKu28rwTfiHVhdavhB0qH0INV6P1lEA== + dependencies: + node-forge "^0.10.0" + +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.5.0, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.4, semver@^7.3.7: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + +send@0.18.0, send@latest: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@6.0.0, serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +set-versions@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/set-versions/-/set-versions-1.0.3.tgz#5915a56b5cbd643e080a24f2e39abf3cda1a73ed" + integrity sha512-fUs9oyv3U863iA/Gcjx/jV2WpC6yHhXKXNM0+oMKL6ZRMsg2jQIijcT8AuZJiPTxi3a3876rI8LZgSHsmV8rCg== + dependencies: + glob "^7.1.4" + glob-promise "^3.4.0" + meow "^5.0.0" + read-pkg-up "^6.0.0" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg== + dependencies: + shebang-regex "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ== + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shiki@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.11.1.tgz#df0f719e7ab592c484d8b73ec10e215a503ab8cc" + integrity sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA== + dependencies: + jsonc-parser "^3.0.0" + vscode-oniguruma "^1.6.1" + vscode-textmate "^6.0.0" + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + integrity sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg== + +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +smoothscroll-polyfill@^0.4.3: + version "0.4.4" + resolved "https://registry.yarnpkg.com/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz#3a259131dc6930e6ca80003e1cb03b603b69abf8" + integrity sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +sockjs-client@^1.5.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.6.1.tgz#350b8eda42d6d52ddc030c39943364c11dcad806" + integrity sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw== + dependencies: + debug "^3.2.7" + eventsource "^2.0.2" + faye-websocket "^0.11.4" + inherits "^2.0.4" + url-parse "^1.5.10" + +sockjs@^0.3.21: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + integrity sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg== + dependencies: + is-plain-obj "^1.0.0" + +sort-object-keys@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" + integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== + +sort-package-json@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.1.0.tgz#6daeb9350f1ac8351a7d58fc2c7c2a6a53d435a8" + integrity sha512-M5ctkdnn7znAkoVQJ0Y+PHDUieyXMhydPyW7r2J9ZM0Iwc3BTyEf5cmoSRfHNo07FEvzWwnphcP7GlrAX164UQ== + dependencies: + detect-indent "^7.0.1" + detect-newline "^4.0.0" + git-hooks-list "^3.0.0" + globby "^13.1.2" + is-plain-obj "^4.1.0" + sort-object-keys "^1.1.3" + +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@~0.5.12: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== + +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA== + +source-map@0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.3: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.12" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz#69077835abe2710b65f03969898b6637b505a779" + integrity sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +split@0.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" + integrity sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA== + dependencies: + through "2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +sshpk@^1.7.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +ssri@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" + integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== + dependencies: + figgy-pudding "^3.5.1" + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +stack-utils@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.5.tgz#a19b0b01947e0029c8e451d5d61a498f5bb1471b" + integrity sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ== + dependencies: + escape-string-regexp "^2.0.0" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g== + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +std-env@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-2.3.1.tgz#d42271908819c243f8defc77a140fc1fcee336a1" + integrity sha512-eOsoKTWnr6C8aWrqJJ2KAReXoa7Vn5Ywyw6uCXgA/xDhxPoaIsBa5aNJmISY04dLwXPBnDHW4diGM7Sn5K4R/g== + dependencies: + ci-info "^3.1.1" + +stream-browserify@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-combiner@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" + integrity sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw== + dependencies: + duplexer "~0.1.1" + +stream-each@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" + integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ== + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string.prototype.trimend@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string.prototype.trimstart@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + +string_decoder@^1.0.0, string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" + integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-indent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" + integrity sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + +style-search@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" + integrity sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg== + +stylehacks@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" + integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== + dependencies: + browserslist "^4.0.0" + postcss "^7.0.0" + postcss-selector-parser "^3.0.0" + +stylelint-config-recommended-scss@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-8.0.0.tgz#1c1e93e619fe2275d4c1067928d92e0614f7d64f" + integrity sha512-BxjxEzRaZoQb7Iinc3p92GS6zRdRAkIuEu2ZFLTxJK2e1AIcCb5B5MXY9KOXdGTnYFZ+KKx6R4Fv9zU6CtMYPQ== + dependencies: + postcss-scss "^4.0.2" + stylelint-config-recommended "^9.0.0" + stylelint-scss "^4.0.0" + +stylelint-config-recommended@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-9.0.0.tgz#1c9e07536a8cd875405f8ecef7314916d94e7e40" + integrity sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ== + +stylelint-config-standard-scss@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard-scss/-/stylelint-config-standard-scss-6.1.0.tgz#a6cddd2a9430578b92fc89726a59474d5548a444" + integrity sha512-iZ2B5kQT2G3rUzx+437cEpdcnFOQkwnwqXuY8Z0QUwIHQVE8mnYChGAquyKFUKZRZ0pRnrciARlPaR1RBtPb0Q== + dependencies: + stylelint-config-recommended-scss "^8.0.0" + stylelint-config-standard "^29.0.0" + +stylelint-config-standard@^29.0.0: + version "29.0.0" + resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-29.0.0.tgz#4cc0e0f05512a39bb8b8e97853247d3a95d66fa2" + integrity sha512-uy8tZLbfq6ZrXy4JKu3W+7lYLgRQBxYTUUB88vPgQ+ZzAxdrvcaSUW9hOMNLYBnwH+9Kkj19M2DHdZ4gKwI7tg== + dependencies: + stylelint-config-recommended "^9.0.0" + +stylelint-scss@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/stylelint-scss/-/stylelint-scss-4.3.0.tgz#638800faf823db11fff60d537c81051fe74c90fa" + integrity sha512-GvSaKCA3tipzZHoz+nNO7S02ZqOsdBzMiCx9poSmLlb3tdJlGddEX/8QzCOD8O7GQan9bjsvLMsO5xiw6IhhIQ== + dependencies: + lodash "^4.17.21" + postcss-media-query-parser "^0.2.3" + postcss-resolve-nested-selector "^0.1.1" + postcss-selector-parser "^6.0.6" + postcss-value-parser "^4.1.0" + +stylelint@^14.16.0: + version "14.16.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-14.16.0.tgz#8e1a424f4b9852e59089f95de306734d70e5048b" + integrity sha512-X6uTi9DcxjzLV8ZUAjit1vsRtSwcls0nl07c9rqOPzvpA8IvTX/xWEkBRowS0ffevRrqkHa/ThDEu86u73FQDg== + dependencies: + "@csstools/selector-specificity" "^2.0.2" + balanced-match "^2.0.0" + colord "^2.9.3" + cosmiconfig "^7.1.0" + css-functions-list "^3.1.0" + debug "^4.3.4" + fast-glob "^3.2.12" + fastest-levenshtein "^1.0.16" + file-entry-cache "^6.0.1" + global-modules "^2.0.0" + globby "^11.1.0" + globjoin "^0.1.4" + html-tags "^3.2.0" + ignore "^5.2.1" + import-lazy "^4.0.0" + imurmurhash "^0.1.4" + is-plain-object "^5.0.0" + known-css-properties "^0.26.0" + mathml-tag-names "^2.1.3" + meow "^9.0.0" + micromatch "^4.0.5" + normalize-path "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.19" + postcss-media-query-parser "^0.2.3" + postcss-resolve-nested-selector "^0.1.1" + postcss-safe-parser "^6.0.0" + postcss-selector-parser "^6.0.11" + postcss-value-parser "^4.2.0" + resolve-from "^5.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + style-search "^0.1.0" + supports-hyperlinks "^2.3.0" + svg-tags "^1.0.0" + table "^6.8.1" + v8-compile-cache "^2.3.0" + write-file-atomic "^4.0.2" + +stylus-loader@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/stylus-loader/-/stylus-loader-3.0.2.tgz#27a706420b05a38e038e7cacb153578d450513c6" + integrity sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA== + dependencies: + loader-utils "^1.0.2" + lodash.clonedeep "^4.5.0" + when "~3.6.x" + +stylus@^0.54.8: + version "0.54.8" + resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.54.8.tgz#3da3e65966bc567a7b044bfe0eece653e099d147" + integrity sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg== + dependencies: + css-parse "~2.0.0" + debug "~3.1.0" + glob "^7.1.6" + mkdirp "~1.0.4" + safer-buffer "^2.1.2" + sax "~1.2.4" + semver "^6.3.0" + source-map "^0.7.3" + +sucrase@^3.20.3: + version "3.29.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.29.0.tgz#3207c5bc1b980fdae1e539df3f8a8a518236da7d" + integrity sha512-bZPAuGA5SdFHuzqIhTAqt9fvNEo9rESqXIG3oiKdF8K4UmkQxC4KlNL3lVyAErXp+mPvUqZ5l13qx6TrDIGf3A== + dependencies: + commander "^4.0.0" + glob "7.1.6" + lines-and-columns "^1.1.6" + mz "^2.7.0" + pirates "^4.0.1" + ts-interface-checker "^0.1.9" + +supports-color@8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-hyperlinks@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" + integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-tags@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" + integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA== + +svgo@^1.0.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== + dependencies: + chalk "^2.4.1" + coa "^2.0.2" + css-select "^2.0.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.37" + csso "^4.0.2" + js-yaml "^3.13.1" + mkdirp "~0.5.1" + object.values "^1.1.0" + sax "~1.2.4" + stable "^0.1.8" + unquote "~1.1.1" + util.promisify "~1.0.0" + +table@^6.8.1: + version "6.8.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" + integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + +tapable@^1.0.0, tapable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +term-size@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" + integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== + +terser-webpack-plugin@^1.4.3: + version "1.4.5" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" + integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw== + dependencies: + cacache "^12.0.2" + find-cache-dir "^2.1.0" + is-wsl "^1.1.0" + schema-utils "^1.0.0" + serialize-javascript "^4.0.0" + source-map "^0.6.1" + terser "^4.1.2" + webpack-sources "^1.4.0" + worker-farm "^1.7.0" + +terser@^4.1.2: + version "4.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" + integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +textextensions@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.6.0.tgz#d7e4ab13fe54e32e08873be40d51b74229b00fc4" + integrity sha512-49WtAWS+tcsy93dRt6P0P3AMD2m5PvXRhuEA0kaXos5ZLlujtYmpmFsB+QvWUSxE1ZsstmYXfQ7L40+EcQgpAQ== + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +three@^0.147.0: + version "0.147.0" + resolved "https://registry.yarnpkg.com/three/-/three-0.147.0.tgz#1974af9e8e0c1efb3a8561334d57c0b6c29a7951" + integrity sha512-LPTOslYQXFkmvceQjFTNnVVli2LaVF6C99Pv34fJypp8NbQLbTlu3KinZ0zURghS5zEehK+VQyvWuPZ/Sm8fzw== + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through@2, through@~2.3, through@~2.3.1, through@~2.3.4: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +timers-browserify@^2.0.4: + version "2.0.12" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" + integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== + dependencies: + setimmediate "^1.0.4" + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A== + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + integrity sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA== + +to-factory@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-factory/-/to-factory-1.0.0.tgz#8738af8bd97120ad1d4047972ada5563bf9479b1" + integrity sha512-JVYrY42wMG7ddf+wBUQR/uHGbjUHZbLisJ8N62AMm0iTZ0p8YTcZLzdtomU0+H+wa99VbkyvQGB3zxB7NDzgIQ== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg== + dependencies: + kind-of "^3.0.2" + +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg== + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + +toposort@^1.0.0: + version "1.0.7" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" + integrity sha512-FclLrw8b9bMWf4QlCJuHBEVhSRsqDj6u3nIjAzPeJvgl//1hBlffdlk0MALceL14+koWEdU4ofRAXofbODxQzg== + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== + dependencies: + punycode "^2.1.0" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +trim-newlines@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" + integrity sha512-MTBWv3jhVjTU7XR3IQHllbiJs8sc75a80OEhB6or/q7pLTWgQ0bMGQXXYQSrSuXe6WiKWDZ5txXY5P59a/coVA== + +trim-newlines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" + integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== + +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsup@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/tsup/-/tsup-6.5.0.tgz#1be97481b7a56385b7c40d01bdabb4196f3649cf" + integrity sha512-36u82r7rYqRHFkD15R20Cd4ercPkbYmuvRkz3Q1LCm5BsiFNUgpo36zbjVhCOgvjyxNBWNKHsaD5Rl8SykfzNA== + dependencies: + bundle-require "^3.1.2" + cac "^6.7.12" + chokidar "^3.5.1" + debug "^4.3.1" + esbuild "^0.15.1" + execa "^5.0.0" + globby "^11.0.3" + joycon "^3.0.1" + postcss-load-config "^3.0.1" + resolve-from "^5.0.0" + rollup "^3.2.5" + source-map "0.8.0-beta.0" + sucrase "^3.20.3" + tree-kill "^1.2.2" + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + integrity sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +turbo-darwin-64@1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.6.3.tgz#fad7e078784b0fafc0b1f75ce9378828918595f5" + integrity sha512-QmDIX0Yh1wYQl0bUS0gGWwNxpJwrzZU2GIAYt3aOKoirWA2ecnyb3R6ludcS1znfNV2MfunP+l8E3ncxUHwtjA== + +turbo-darwin-arm64@1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-1.6.3.tgz#f0a32cae39e3fcd3da5e3129a94c18bb2e3ed6aa" + integrity sha512-75DXhFpwE7CinBbtxTxH08EcWrxYSPFow3NaeFwsG8aymkWXF+U2aukYHJA6I12n9/dGqf7yRXzkF0S/9UtdyQ== + +turbo-linux-64@1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.6.3.tgz#8ddc6ac55ef84641182fe5ff50647f1b355826b0" + integrity sha512-O9uc6J0yoRPWdPg9THRQi69K6E2iZ98cRHNvus05lZbcPzZTxJYkYGb5iagCmCW/pq6fL4T4oLWAd6evg2LGQA== + +turbo-linux-arm64@1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.6.3.tgz#846c1dc84d8dc741651906613c16acccba30428c" + integrity sha512-dCy667qqEtZIhulsRTe8hhWQNCJO0i20uHXv7KjLHuFZGCeMbWxB8rsneRoY+blf8+QNqGuXQJxak7ayjHLxiA== + +turbo-windows-64@1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.6.3.tgz#89ac819fa76ad31d12fbfdeb3045bcebd0d308eb" + integrity sha512-lKRqwL3mrVF09b9KySSaOwetehmGknV9EcQTF7d2dxngGYYX1WXoQLjFP9YYH8ZV07oPm+RUOAKSCQuDuMNhiA== + +turbo-windows-arm64@1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-1.6.3.tgz#977607c9a51f0b76076c8b158bafce06ce813070" + integrity sha512-BXY1sDPEA1DgPwuENvDCD8B7Hb0toscjus941WpL8CVd10hg9pk/MWn9CNgwDO5Q9ks0mw+liDv2EMnleEjeNA== + +turbo@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.6.3.tgz#ec26cc8907c38a9fd6eb072fb10dad254733543e" + integrity sha512-FtfhJLmEEtHveGxW4Ye/QuY85AnZ2ZNVgkTBswoap7UMHB1+oI4diHPNyqrQLG4K1UFtCkjOlVoLsllUh/9QRw== + optionalDependencies: + turbo-darwin-64 "1.6.3" + turbo-darwin-arm64 "1.6.3" + turbo-linux-64 "1.6.3" + turbo-linux-arm64 "1.6.3" + turbo-windows-64 "1.6.3" + turbo-windows-arm64 "1.6.3" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.18.0: + version "0.18.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" + integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" + integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +typedoc-plugin-extras@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/typedoc-plugin-extras/-/typedoc-plugin-extras-2.3.1.tgz#cfbfea79fb46946599c1cc57d889b5b31bfecc6d" + integrity sha512-EqrIX9oOxJ3yZa/TNAmMqvg87v3RTs63yFm/ROJtjiu1yVNQl74v2hDHsgheWb+1Puy+Vu5jFMwx//Cme0hzqg== + +typedoc-plugin-resolve-crossmodule-references@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/typedoc-plugin-resolve-crossmodule-references/-/typedoc-plugin-resolve-crossmodule-references-0.3.2.tgz#d4c9ce2609c60ad5cea2426e9fab95bbf5884adc" + integrity sha512-28NJzaA+OxEkHLyaOnTAOW6GC6A30f00LSgFZ7x+haFb2Lhlpno4Aiocbf5RRUQ1uht/O4c47KbYm837HsO/nw== + +typedoc@^0.23.21: + version "0.23.21" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.23.21.tgz#2a6b0e155f91ffa9689086706ad7e3e4bc11d241" + integrity sha512-VNE9Jv7BgclvyH9moi2mluneSviD43dCE9pY8RWkO88/DrEgJZk9KpUk7WO468c9WWs/+aG6dOnoH7ccjnErhg== + dependencies: + lunr "^2.3.9" + marked "^4.0.19" + minimatch "^5.1.0" + shiki "^0.11.1" + +typescript@^4.9.3: + version "4.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" + integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== + +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + +uglify-js@3.4.x: + version "3.4.10" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f" + integrity sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw== + dependencies: + commander "~2.19.0" + source-map "~0.6.1" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + integrity sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA== + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + integrity sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ== + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unix-crypt-td-js@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz#4912dfad1c8aeb7d20fa0a39e4c31918c1d5d5dd" + integrity sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" + integrity sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg== + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ== + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.1.0, upath@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +update-browserslist-db@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +update-notifier@^4.0.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" + integrity sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A== + dependencies: + boxen "^4.2.0" + chalk "^3.0.0" + configstore "^5.0.1" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.3.1" + is-npm "^4.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.0.0" + pupa "^2.0.1" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" + +upper-case@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + integrity sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg== + +url-loader@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8" + integrity sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg== + dependencies: + loader-utils "^1.1.0" + mime "^2.0.3" + schema-utils "^1.0.0" + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ== + dependencies: + prepend-http "^2.0.0" + +url-parse@^1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ== + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +util.promisify@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" + integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + dependencies: + define-properties "^1.1.2" + object.getownpropertydescriptors "^2.0.3" + +util.promisify@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + integrity sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ== + dependencies: + inherits "2.0.1" + +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== + dependencies: + inherits "2.0.3" + +utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^3.0.0, uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +v8-compile-cache@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vendors@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" + integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm-browserify@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== + +vscode-oniguruma@^1.6.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz#aeb9771a2f1dbfc9083c8a7fdd9cccaa3f386607" + integrity sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA== + +vscode-textmate@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-6.0.0.tgz#a3777197235036814ac9a92451492f2748589210" + integrity sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ== + +vue-class-component@^7.1.0: + version "7.2.6" + resolved "https://registry.yarnpkg.com/vue-class-component/-/vue-class-component-7.2.6.tgz#8471e037b8e4762f5a464686e19e5afc708502e4" + integrity sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w== + +vue-github-button@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/vue-github-button/-/vue-github-button-1.3.0.tgz#a4da317a3b93d05824768b3916a0d22fa91603be" + integrity sha512-Cc92t+GBLwBPhwtHSvKXjbx07U3+6xdi+eR+s9c734tHbndipCLenJjLVkgErNhKZ0EvDjRyuu8Hu69gg9/TxQ== + dependencies: + github-buttons "^2.8.0" + +vue-github-buttons@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/vue-github-buttons/-/vue-github-buttons-3.1.0.tgz#eaea2ba0b7e0df5a7fd1c61ba37dabf7553dd79a" + integrity sha512-x0b9bdhP5xZOD5kQ9+nnCzvKqVyHb4moqN2l06mjYB/k2WRdW5jiAWlneUgoPFwPvcqM40vrTDXVvBrS0MMlEQ== + dependencies: + format-thousands "^1.1.1" + node-fetch "^2.3.0" + tslib "^1.9.3" + +vue-hot-reload-api@^2.3.0: + version "2.3.4" + resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2" + integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog== + +vue-loader@^15.7.1: + version "15.10.1" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.10.1.tgz#c451c4cd05a911aae7b5dbbbc09fb913fb3cca18" + integrity sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA== + dependencies: + "@vue/component-compiler-utils" "^3.1.0" + hash-sum "^1.0.2" + loader-utils "^1.1.0" + vue-hot-reload-api "^2.3.0" + vue-style-loader "^4.1.0" + +vue-material@^1.0.0-beta-15: + version "1.0.0-beta-15" + resolved "https://registry.yarnpkg.com/vue-material/-/vue-material-1.0.0-beta-15.tgz#949025464f8fe2ff3b9be2ba1365d9eab770ad8a" + integrity sha512-nNC1mF1BQNKsyEjRXPYxweYlIOcVE9rK4LeeyppOU6h4vgQnZuNmlGIRnl6fUe8dj+x7c5x5/qydLhJRabPMng== + dependencies: + opencollective-postinstall "^2.0.2" + vue-github-button "^1.2.0" + vue-github-buttons "^3.1.0" + vue-toc "0.0.1" + +vue-no-ssr@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz#875f3be6fb0ae41568a837f3ac1a80eaa137b998" + integrity sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g== + +vue-property-decorator@^8.0.0: + version "8.5.1" + resolved "https://registry.yarnpkg.com/vue-property-decorator/-/vue-property-decorator-8.5.1.tgz#571a91cf8d2b507f537d79bf8275af3184572fff" + integrity sha512-O6OUN2OMsYTGPvgFtXeBU3jPnX5ffQ9V4I1WfxFQ6dqz6cOUbR3Usou7kgFpfiXDvV7dJQSFcJ5yUPgOtPPm1Q== + dependencies: + vue-class-component "^7.1.0" + +vue-router@^3.4.5: + version "3.6.5" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.6.5.tgz#95847d52b9a7e3f1361cb605c8e6441f202afad8" + integrity sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ== + +vue-server-renderer@^2.6.10: + version "2.7.14" + resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.7.14.tgz#986f3fdca63fbb38bb6834698f11e0d6a81f182f" + integrity sha512-NlGFn24tnUrj7Sqb8njhIhWREuCJcM3140aMunLNcx951BHG8j3XOrPP7psSCaFA8z6L4IWEjudztdwTp1CBVw== + dependencies: + chalk "^4.1.2" + hash-sum "^2.0.0" + he "^1.2.0" + lodash.template "^4.5.0" + lodash.uniq "^4.5.0" + resolve "^1.22.0" + serialize-javascript "^6.0.0" + source-map "0.5.6" + +vue-slider-component@^3.2.23: + version "3.2.23" + resolved "https://registry.yarnpkg.com/vue-slider-component/-/vue-slider-component-3.2.23.tgz#58a6bae1f2262befe0dba77091d61e1320b8d53f" + integrity sha512-/AyoIxJoflCNB0aaHXVw+3iFzG6cV77XZBvRFBcJq/j55u+qQ2iQb0CEgud6Bm/6sonSVncYKjW0cnpKlrQ2HA== + dependencies: + core-js "^3.6.5" + vue-property-decorator "^8.0.0" + +vue-style-loader@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35" + integrity sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg== + dependencies: + hash-sum "^1.0.2" + loader-utils "^1.0.2" + +vue-swatches@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/vue-swatches/-/vue-swatches-2.1.1.tgz#26c467fb7648ff4ee0887aea36d1e03b15032b83" + integrity sha512-YugkNbByxMz1dnx1nZyHSL3VSf/TnBH3/NQD+t8JKxPSqUmX87sVGBxjEaqH5IMraOLfVmU0pHCHl2BfXNypQg== + +vue-template-compiler@^2.6.10: + version "2.7.14" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz#4545b7dfb88090744c1577ae5ac3f964e61634b1" + integrity sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ== + dependencies: + de-indent "^1.0.2" + he "^1.2.0" + +vue-template-es2015-compiler@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" + integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== + +vue-toc@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/vue-toc/-/vue-toc-0.0.1.tgz#6a4dfa9c144445679705cd7b991a1a73ac080cc0" + integrity sha512-RZfVgLzk/kpEmk05ptvU/+x3TVo4Ai4BBARvV4iCurR9bJsAqnnrqwjEBKnEG+s6NT0yQ6EY0JMGViyOUGysDw== + dependencies: + vue "^2.6.10" + +vue@^2.6.10: + version "2.7.14" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.14.tgz#3743dcd248fd3a34d421ae456b864a0246bafb17" + integrity sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ== + dependencies: + "@vue/compiler-sfc" "2.7.14" + csstype "^3.1.0" + +vuepress-html-webpack-plugin@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/vuepress-html-webpack-plugin/-/vuepress-html-webpack-plugin-3.2.0.tgz#219be272ad510faa8750d2d4e70fd028bfd1c16e" + integrity sha512-BebAEl1BmWlro3+VyDhIOCY6Gef2MCBllEVAP3NUAtMguiyOwo/dClbwJ167WYmcxHJKLl7b0Chr9H7fpn1d0A== + dependencies: + html-minifier "^3.2.3" + loader-utils "^0.2.16" + lodash "^4.17.3" + pretty-error "^2.0.2" + tapable "^1.0.0" + toposort "^1.0.0" + util.promisify "1.0.0" + +vuepress-plugin-container@^2.0.2: + version "2.1.5" + resolved "https://registry.yarnpkg.com/vuepress-plugin-container/-/vuepress-plugin-container-2.1.5.tgz#37fff05662fedbd63ffd3a5463b2592c7a7f3133" + integrity sha512-TQrDX/v+WHOihj3jpilVnjXu9RcTm6m8tzljNJwYhxnJUW0WWQ0hFLcDTqTBwgKIFdEiSxVOmYE+bJX/sq46MA== + dependencies: + "@vuepress/shared-utils" "^1.2.0" + markdown-it-container "^2.0.0" + +vuepress-plugin-smooth-scroll@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/vuepress-plugin-smooth-scroll/-/vuepress-plugin-smooth-scroll-0.0.3.tgz#6eff2d4c186cca917cc9f7df2b0af7de7c8c6438" + integrity sha512-qsQkDftLVFLe8BiviIHaLV0Ea38YLZKKonDGsNQy1IE0wllFpFIEldWD8frWZtDFdx6b/O3KDMgVQ0qp5NjJCg== + dependencies: + smoothscroll-polyfill "^0.4.3" + +vuepress@^1.9.7: + version "1.9.7" + resolved "https://registry.yarnpkg.com/vuepress/-/vuepress-1.9.7.tgz#2cd6709a2228f5cef588115aaeabf820ab9ed7cc" + integrity sha512-aSXpoJBGhgjaWUsT1Zs/ZO8JdDWWsxZRlVme/E7QYpn+ZB9iunSgPMozJQNFaHzcRq4kPx5A4k9UhzLRcvtdMg== + dependencies: + "@vuepress/core" "1.9.7" + "@vuepress/theme-default" "1.9.7" + "@vuepress/types" "1.9.7" + cac "^6.5.6" + envinfo "^7.2.0" + opencollective-postinstall "^2.0.2" + update-notifier "^4.0.0" + +watchpack-chokidar2@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" + integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww== + dependencies: + chokidar "^2.1.8" + +watchpack@^1.7.4: + version "1.7.5" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" + integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== + dependencies: + graceful-fs "^4.1.2" + neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.1" + watchpack-chokidar2 "^2.0.1" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +webpack-chain@^4.9.0: + version "4.12.1" + resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-4.12.1.tgz#6c8439bbb2ab550952d60e1ea9319141906c02a6" + integrity sha512-BCfKo2YkDe2ByqkEWe1Rw+zko4LsyS75LVr29C6xIrxAg9JHJ4pl8kaIZ396SUSNp6b4815dRZPSTAS8LlURRQ== + dependencies: + deepmerge "^1.5.2" + javascript-stringify "^1.6.0" + +webpack-chain@^6.0.0: + version "6.5.1" + resolved "https://registry.yarnpkg.com/webpack-chain/-/webpack-chain-6.5.1.tgz#4f27284cbbb637e3c8fbdef43eef588d4d861206" + integrity sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA== + dependencies: + deepmerge "^1.5.2" + javascript-stringify "^2.0.1" + +webpack-dev-middleware@^3.7.2: + version "3.7.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" + integrity sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ== + dependencies: + memory-fs "^0.4.1" + mime "^2.4.4" + mkdirp "^0.5.1" + range-parser "^1.2.1" + webpack-log "^2.0.0" + +webpack-dev-server@^3.5.1: + version "3.11.3" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.3.tgz#8c86b9d2812bf135d3c9bce6f07b718e30f7c3d3" + integrity sha512-3x31rjbEQWKMNzacUZRE6wXvUFuGpH7vr0lIEbYpMAG9BOxi0928QU1BBswOAP3kg3H1O4hiS+sq4YyAn6ANnA== + dependencies: + ansi-html-community "0.0.8" + bonjour "^3.5.0" + chokidar "^2.1.8" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.1" + express "^4.17.1" + html-entities "^1.3.1" + http-proxy-middleware "0.19.1" + import-local "^2.0.0" + internal-ip "^4.3.0" + ip "^1.1.5" + is-absolute-url "^3.0.3" + killable "^1.0.1" + loglevel "^1.6.8" + opn "^5.5.0" + p-retry "^3.0.1" + portfinder "^1.0.26" + schema-utils "^1.0.0" + selfsigned "^1.10.8" + semver "^6.3.0" + serve-index "^1.9.1" + sockjs "^0.3.21" + sockjs-client "^1.5.0" + spdy "^4.0.2" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.7.2" + webpack-log "^2.0.0" + ws "^6.2.1" + yargs "^13.3.2" + +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" + integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== + dependencies: + ansi-colors "^3.0.0" + uuid "^3.3.2" + +webpack-merge@^4.1.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.2.tgz#a27c52ea783d1398afd2087f547d7b9d2f43634d" + integrity sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g== + dependencies: + lodash "^4.17.15" + +webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@^4.8.1: + version "4.46.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542" + integrity sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.4.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.5.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.7.4" + webpack-sources "^1.4.1" + +webpackbar@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-3.2.0.tgz#bdaad103fad11a4e612500e72aaae98b08ba493f" + integrity sha512-PC4o+1c8gWWileUfwabe0gqptlXUDJd5E0zbpr2xHP1VSOVlZVPBZ8j6NCR8zM5zbKdxPhctHXahgpNK1qFDPw== + dependencies: + ansi-escapes "^4.1.0" + chalk "^2.4.1" + consola "^2.6.0" + figures "^3.0.0" + pretty-time "^1.1.0" + std-env "^2.2.1" + text-table "^0.2.0" + wrap-ansi "^5.1.0" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +when@~3.6.x: + version "3.6.4" + resolved "https://registry.yarnpkg.com/when/-/when-3.6.4.tgz#473b517ec159e2b85005497a13983f095412e34e" + integrity sha512-d1VUP9F96w664lKINMGeElWdhhb5sC+thXM+ydZGU3ZnaE09Wv6FaS+mpM9570kcDs/xMfcXJBTLsMdHEFYY9Q== + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q== + +which@^1.2.9, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +worker-farm@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" + integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== + dependencies: + errno "~0.1.7" + +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +ws@^6.2.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" + integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== + dependencies: + async-limiter "~1.0.0" + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + +xtend@^4.0.0, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0, yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yaml@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.3.tgz#9b3a4c8aff9821b696275c79a8bee8399d945207" + integrity sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + +yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^20.2.2, yargs-parser@^20.2.3: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@^13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zepto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/zepto/-/zepto-1.2.0.tgz#e127bd9e66fd846be5eab48c1394882f7c0e4f98" + integrity sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==