├── .commitlintrc.js
├── .cz-config.js
├── .eslintignore
├── .eslintrc.js
├── .github
└── FUNDING.yml
├── .gitignore
├── .huskyrc.js
├── .prettierrc
├── CHANGELOG.md
├── CHANGELOG.zh-CN.md
├── LICENSE
├── README.md
├── README.zh-CN.md
├── babel.config.js
├── docs
├── .nojekyll
├── _navbar.md
├── en-us
│ ├── _sidebar.md
│ ├── components.md
│ └── krpano-action-proxy.md
├── index.html
└── zh-cn
│ ├── _sidebar.md
│ ├── components.md
│ └── krpano-action-proxy.md
├── jest.config.js
├── krpano-lib
├── krpano.js
└── krpano.swf
├── lint-staged.config.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── scripts
└── syncDoc.ts
├── src
├── KrpanoActionProxy.ts
├── __snapshots__
│ └── utils.test.ts.snap
├── components
│ ├── Events.tsx
│ ├── Hotspot.tsx
│ ├── Krpano.tsx
│ ├── Scene.tsx
│ └── View.tsx
├── contexts
│ ├── CurrentSceneContext.ts
│ └── KrpanoRendererContext.ts
├── hooks
│ ├── useEventCallback.ts
│ └── useKrpano.ts
├── index.ts
├── types.ts
├── utils.test.ts
└── utils.ts
├── tsconfig.json
└── tsconfig.typing.json
/.commitlintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['cz'],
3 | rules: {},
4 | };
5 |
--------------------------------------------------------------------------------
/.cz-config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | types: [
5 | {
6 | value: 'WIP',
7 | name: '💪 WIP: Work in progress',
8 | },
9 | {
10 | value: 'feat',
11 | name: '✨ feat: A new feature',
12 | },
13 | {
14 | value: 'fix',
15 | name: '🐞 fix: A bug fix',
16 | },
17 | {
18 | value: 'refactor',
19 | name: '🛠 refactor: A code change that neither fixes a bug nor adds a feature',
20 | },
21 | {
22 | value: 'docs',
23 | name: '📚 docs: Documentation only changes',
24 | },
25 | {
26 | value: 'test',
27 | name: '🏁 test: Add missing tests or correcting existing tests',
28 | },
29 | {
30 | value: 'chore',
31 | name:
32 | "🗯 chore: Changes that don't modify src or test files. Such as updating build tasks, package manager",
33 | },
34 | {
35 | value: 'style',
36 | name:
37 | '💅 style: Code Style, Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
38 | },
39 | {
40 | value: 'revert',
41 | name: '⏪ revert: Revert to a commit',
42 | },
43 | ],
44 |
45 | scopes: [],
46 |
47 | allowCustomScopes: true,
48 | allowBreakingChanges: ['feat', 'fix'],
49 | };
50 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | node_modules/
4 | .snapshots/
5 | *.min.js
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('@types/eslint').Linter.BaseConfig}
3 | */
4 | module.exports = {
5 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser
6 | extends: [
7 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
8 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
9 | 'plugin:prettier/recommended',
10 | ],
11 | parserOptions: {
12 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
13 | sourceType: 'module', // Allows for the use of imports
14 | ecmaFeatures: {
15 | node: true,
16 | },
17 | },
18 | settings: {
19 | react: {
20 | version: 'detect',
21 | },
22 | },
23 | plugins: ['react-hooks'],
24 | rules: {
25 | // typescript
26 | '@typescript-eslint/no-parameter-properties': 'off',
27 | '@typescript-eslint/ban-ts-comment': 'off',
28 | // 不限制
29 | '@typescript-eslint/explicit-member-accessibility': 'off',
30 | '@typescript-eslint/no-empty-interface': 'off',
31 | '@typescript-eslint/no-non-null-assertion': 'warn',
32 | '@typescript-eslint/array-type': ['warn', { default: 'array-simple' }],
33 | '@typescript-eslint/interface-name-prefix': 'off',
34 | '@typescript-eslint/no-object-literal-type-assertion': 'off',
35 | '@typescript-eslint/explicit-function-return-type': [
36 | 'off',
37 | { allowExpressions: true, allowTypedFunctionExpressions: true },
38 | ],
39 |
40 | // hooks
41 | 'react-hooks/rules-of-hooks': 'error',
42 | 'react-hooks/exhaustive-deps': 'warn',
43 |
44 | // misc
45 | 'no-implicit-coercion': 'off',
46 | 'no-useless-escape': 'off',
47 | 'array-callback-return': 'off',
48 | 'no-useless-constructor': 'off',
49 | 'no-empty-function': ['error', { allow: ['constructors'] }],
50 | quotes: ['error', 'single', { allowTemplateLiterals: true }],
51 | 'no-undef': 'off',
52 | },
53 | overrides: [
54 | {
55 | files: ['**/*.js'],
56 | excludedFiles: ['node_modules', './dist/**/*'],
57 | rules: {
58 | '@typescript-eslint/no-unused-vars': 'warn',
59 | '@typescript-eslint/no-var-requires': 'off',
60 | '@typescript-eslint/camelcase': 'off',
61 | },
62 | },
63 | {
64 | files: ['**/*.test.tsx'],
65 | excludedFiles: ['node_modules', './dist/**/*'],
66 | env: {
67 | node: true,
68 | },
69 | },
70 | ],
71 | };
72 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: ['https://www.buymeacoffee.com/reactkrpano']# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | *.tsbuildinfo
107 |
--------------------------------------------------------------------------------
/.huskyrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | hooks: {
3 | 'pre-commit': 'lint-staged',
4 | // 'prepare-commit-msg': 'exec < /dev/tty && git cz --hook || true',
5 | 'commit-msg': 'commitlint -e $GIT_PARAMS',
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "endOfLine": "lf",
3 | "semi": true,
4 | "trailingComma": "es5",
5 | "singleQuote": true,
6 | "printWidth": 120,
7 | "tabWidth": 4,
8 | "arrowParens": "avoid",
9 | "overrides": [
10 | {
11 | "files": ["*.json"],
12 | "options": {
13 | "tabWidth": 2
14 | }
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [0.1.5](https://github.com/0xLLLLH/react-krpano/compare/v0.1.3...v0.1.5) (2022-02-22)
6 |
7 | ### Bug Fixes
8 |
9 | - 🐞 fix CommonJS build dependency problem ([b90ba0b](https://github.com/0xLLLLH/react-krpano/commit/b90ba0bca9648b84b35b735c05f8303418b29364))
10 |
11 | ## [0.1.4](https://github.com/0xLLLLH/react-krpano/compare/v0.1.3...v0.1.4) (2021-03-03)
12 |
13 | ### Features
14 |
15 | - ✨Krpano component supports enableLogger attribute ([158955f](https://github.com/0xLLLLH/react-krpano/commit/158955fbe77a045a85c9f3867742780d69759050))
16 |
17 | ## [0.1.3](https://github.com/0xLLLLH/react-krpano/compare/v0.1.2...v0.1.3) (2021-01-03)
18 |
19 | ### Features
20 |
21 | - ✨Krpano component add default size ([fa8d678](https://github.com/0xLLLLH/react-krpano/commit/fa8d678228443b5f86a950d0d0548f5758904ab3))
22 |
23 | ### Bug Fixes
24 |
25 | - 🐞Fix the problem that the related interface of each component is not exported ([604eb56](https://github.com/0xLLLLH/react-krpano/commit/604eb56a53389b4e05c5bb51b2d2ed1f4a63c006))
26 |
27 | ## [0.1.2](https://github.com/0xLLLLH/react-krpano/compare/v0.1.1...v0.1.2) (2020-12-19)
28 |
29 | ### Features
30 |
31 | - ✨krpano component supports setting target and id ([cd396af](https://github.com/0xLLLLH/react-krpano/commit/cd396af5537b0f3918013a59d22c516d6f4d8097))
32 |
33 | ## [0.1.1](https://github.com/0xLLLLH/react-krpano/compare/v0.1.0...v0.1.1) (2020-12-17)
34 |
35 | ### Bug Fixes
36 |
37 | - Fix the problem of View component children type ([966c13a](https://github.com/0xLLLLH/react-krpano/commit/966c13a026c3efc75bbde1bb65079c98a073698b))
38 |
39 | ## [0.1.0](https://github.com/0xLLLLH/react-krpano/compare/v0.0.2...v0.1.0) (2020-12-17)
40 |
41 | ### ⚠ BREAKING CHANGES
42 |
43 | - Case adjustment of event name: oncontextmenu->onContextmenu, onAutorotateStart->onAutoRotateStart
44 | - Rename fovmin/fovmax to fovMin/fovMax
45 |
46 | ### Features
47 |
48 | - The view component supports all view tag attributes; fovmin->fovMin ([2b615e2](https://github.com/0xLLLLH/react-krpano/commit/2b615e2381eab8c7241eadffc6ff53619260f851))
49 |
50 | - Events component event name adjustment ([25b0cfb](https://github.com/0xLLLLH/react-krpano/commit/25b0cfb7e4b092d0893ef7af1162e0cc2f8209ed))
51 |
52 | ## 0.0.2 (2020-12-16)
53 |
54 | ### Bug Fixes
55 |
56 | - Fix the problem that KrpanoActionProxy and other content cannot be imported from the index ([11c6a7d](https://github.com/0xLLLLH/react-krpano/commit/11c6a7d3a02cd4631a67ef688e4aa8d76f7b90f8))
57 |
58 | ## 0.0.1 (2020-12-16)
59 |
--------------------------------------------------------------------------------
/CHANGELOG.zh-CN.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | 所有值得记录的修改会在这个文档中留档。查阅[standard-version](https://github.com/conventional-changelog/standard-version)来了解提交规范。
4 |
5 | ### [0.1.5](https://github.com/0xLLLLH/react-krpano/compare/v0.1.3...v0.1.5) (2022-02-22)
6 |
7 | ### Bug Fixes
8 |
9 | - 🐞 修复 CommonJS 构建产出的依赖问题 ([b90ba0b](https://github.com/0xLLLLH/react-krpano/commit/b90ba0bca9648b84b35b735c05f8303418b29364))
10 |
11 | ## [0.1.4](https://github.com/0xLLLLH/react-krpano/compare/v0.1.3...v0.1.4) (2021-03-03)
12 |
13 | ### Features
14 |
15 | - ✨Krpano 组件支持 enableLogger 参数 ([158955f](https://github.com/0xLLLLH/react-krpano/commit/158955fbe77a045a85c9f3867742780d69759050))
16 |
17 | ## [0.1.3](https://github.com/0xLLLLH/react-krpano/compare/v0.1.2...v0.1.3) (2021-01-03)
18 |
19 | ### Features
20 |
21 | - ✨Krpano 组件添加默认大小 ([fa8d678](https://github.com/0xLLLLH/react-krpano/commit/fa8d678228443b5f86a950d0d0548f5758904ab3))
22 |
23 | ### Bug Fixes
24 |
25 | - 🐞 修复各个组件的相关 interface 未导出的问题 ([604eb56](https://github.com/0xLLLLH/react-krpano/commit/604eb56a53389b4e05c5bb51b2d2ed1f4a63c006))
26 |
27 | ## [0.1.2](https://github.com/0xLLLLH/react-krpano/compare/v0.1.1...v0.1.2) (2020-12-19)
28 |
29 | ### Features
30 |
31 | - ✨krpano 组件支持设置 target 和 id ([cd396af](https://github.com/0xLLLLH/react-krpano/commit/cd396af5537b0f3918013a59d22c516d6f4d8097))
32 |
33 | ## [0.1.1](https://github.com/0xLLLLH/react-krpano/compare/v0.1.0...v0.1.1) (2020-12-17)
34 |
35 | ### Bug Fixes
36 |
37 | - 修复 View 组件 children 类型的问题 ([966c13a](https://github.com/0xLLLLH/react-krpano/commit/966c13a026c3efc75bbde1bb65079c98a073698b))
38 |
39 | ## [0.1.0](https://github.com/0xLLLLH/react-krpano/compare/v0.0.2...v0.1.0) (2020-12-17)
40 |
41 | ### ⚠ BREAKING CHANGES
42 |
43 | - 事件名称大小写调整:oncontextmenu->onContextmenu,onAutorotateStart->onAutoRotateStart
44 | - 将 fovmin/fovmax 重命名为 fovMin/fovMax
45 |
46 | ### Features
47 |
48 | - view 组件支持所有 view 标签属性;fovmin->fovMin ([2b615e2](https://github.com/0xLLLLH/react-krpano/commit/2b615e2381eab8c7241eadffc6ff53619260f851))
49 |
50 | - events 组件事件名称调整 ([25b0cfb](https://github.com/0xLLLLH/react-krpano/commit/25b0cfb7e4b092d0893ef7af1162e0cc2f8209ed))
51 |
52 | ## 0.0.2 (2020-12-16)
53 |
54 | ### Bug Fixes
55 |
56 | - 修复无法从 index 引入 KrpanoActionProxy 等内容的问题 ([11c6a7d](https://github.com/0xLLLLH/react-krpano/commit/11c6a7d3a02cd4631a67ef688e4aa8d76f7b90f8))
57 |
58 | ## 0.0.1 (2020-12-16)
59 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 0xLLLLH
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Krpano
2 | > React bindings for krpano.
3 |
4 |
5 | [](https://github.com/0xLLLLH/react-krpano/blob/main/LICENSE)
6 | [](http://commitizen.github.io/cz-cli/)
7 | [![NPM version][npm-image]][npm-url]
8 |
9 | [npm-image]: https://img.shields.io/npm/v/@0xllllh/react-krpano?style=flat-square
10 | [npm-url]: https://www.npmjs.com/package/@0xllllh/react-krpano
11 |
12 | [Demo](https://0xllllh.github.io/react-krpano-examples)
13 | ## ✨ Features
14 | * Dynamic rendering of scenes and hotspots without generating xml
15 | * Use Typescript to develop and provide a complete type definition file.
16 |
17 | ## 🖥 Dependencies
18 |
19 | * krpano.js >= 1.20.9
20 | * React >= 16.8
21 |
22 | ## 📦 Installation
23 |
24 | * With NPM
25 | ``` bash
26 | yarn add @0xllllh/react-krpano
27 | ```
28 | * Download the latest Krpano from [Krpano official website](https://krpano.com/download/) and unzip it to get JS file, then import it through the script tag of your index.html to make the script available globally.
29 | ```html
30 |
31 | ```
32 |
33 | ## 🔨 How to use
34 | ### Loading XML file
35 | The most basic usage is to directly load the krpano xml file through `Krpano` the `xml` parameters of the component . The Krpano component will faithfully render according to the xml configuration.
36 |
37 | **krpano.xml**
38 | ```xml
39 |
40 |
41 |
42 |
43 |
44 |
51 |
52 |
53 |
54 | ```
55 |
56 | **App.css**
57 | ``` css
58 | .App {
59 | width: 600px;
60 | height: 400px;
61 | }
62 | ```
63 |
64 | **App.tsx**
65 | ``` tsx
66 | ReactDOM.render(, document.getElementById('app'));
67 | ```
68 |
69 | ### Scene display and switching
70 | > In order to simplify the implementation and use, the implementation of krpano's image tag has been merged into the Scene component. The images of the scene can be specified through the `images` props of Scene component
71 |
72 | To add a scene, you need to use the Scene component. Each one represents a scene, and active scene can be specified through the `currentScene` prop of Krpano component.
73 |
74 | ```tsx
75 | ReactDOM.render(
76 |
77 |
84 |
91 |
92 | ,
93 | document.getElementById('app'));
94 | ```
95 |
96 | ### Hotspots
97 |
98 | > Currently support only image hotspots
99 |
100 | Hotspots can be easily rendered using Hotspot component. It support a bunch of callback settings
101 |
102 | ```tsx
103 | const App = () => {
104 | const [currentScene, setCurrentScene] = React.useState('scene0');
105 | // Datas
106 | const scenes = [{
107 | name: 'scene0',
108 | previewUrl: '/preview.jpg',
109 | hotspots: [{
110 | name: 'hot',
111 | type: 'image',
112 | url: 'hotspot.png',
113 | ath: 0,
114 | atv: 20,
115 | onClick: () => setCurrentScene('scene1')
116 | }]
117 | },
118 | name: 'scene1',
119 | previewUrl: '/preview.jpg',
120 | hotspots: []
121 | }]
122 |
123 | return (
124 |
125 |
126 | {scenes.map(sc => (
127 |
128 | {sc.hotspots.map(pt => )}
129 |
130 | ))}
131 |
132 | )
133 | }
134 |
135 | ReactDOM.render(, document.getElementById('app'));
136 | ```
137 |
138 | ### Access unsuported features
139 | Since this project has just started development, many components and functions are not yet completed. If there is a function that needs priority support, you can raise an issue on Github. If you want, you can use the `KrpanoActionProxy` to call the krpano functions by yourself after obtaining it.
140 |
141 | Various callback functions will get the KrpanoActionProxy instance as a parameter, and the encapsulated method can be used to control krpano. You can also use `renderer.KrpanoRenderer` to get a native instance of krpano.
142 | ```tsx
143 | const App = () => {
144 | const [currentScene, setCurrentScene] = React.useState('scene0');
145 | const onHotspotClick = React.useCallback((renderer: KrpanoActionProxy) => {
146 | console.log(renderer.get('view.fov'));
147 | setCurrentScene('scene1');
148 | }, []);
149 |
150 | return (
151 | {
155 | console.log('Ready message from App', renderer.krpanoRenderer);
156 | }}
157 | >
158 |
159 |
160 |
168 |
169 |
170 |
171 | );
172 | };
173 | ```
174 |
175 | In addition, tags such as style and action can be written in xml, then imported through `xml` prop of Krpano component.
176 |
177 | **pano.xml**
178 | ```xml
179 |
180 |
181 | ...
182 |
183 | ```
184 | **App.tsx**
185 | ```tsx
186 | const App = () => (
187 |
192 |
193 |
200 |
201 |
202 | );
203 | ```
204 | ## ❗️ Restrictions
205 |
206 | * Only one krpano panorama is displayed on a page at a time. If you need to display multiple panoramas at the same time, a lighter solution will be more appropriate.
207 | * React components only implement part of their functions for the time being.
208 |
209 | ## 🔗 Link
210 | * [Home](https://0xllllh.github.io/react-krpano/)
211 | * [Components Documentation](https://0xllllh.github.io/react-krpano/#/components)
212 | * [Demo project](https://github.com/0xLLLLH/react-krpano-examples)
213 | * [CHANGELOG](/CHANGELOG.md)
214 | * [Krpano official website](https://krpano.com/docu/xml/)
215 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # React Krpano
2 | > React bindings for krpano.
3 |
4 |
5 | [](https://github.com/0xLLLLH/react-krpano/blob/main/LICENSE)
6 | [](http://commitizen.github.io/cz-cli/)
7 | [![NPM version][npm-image]][npm-url]
8 |
9 | [npm-image]: https://img.shields.io/npm/v/@0xllllh/react-krpano?style=flat-square
10 | [npm-url]: https://www.npmjs.com/package/@0xllllh/react-krpano
11 |
12 | [Demo](https://0xllllh.github.io/react-krpano-examples)
13 | ## ✨ 特性
14 | * 动态渲染场景和热点,无需生成xml
15 | * 使用Typescript开发,提供完整的类型定义文件。
16 |
17 | ## 🖥 依赖
18 |
19 | * krpano.js >= 1.20.9
20 | * React >= 16.8
21 |
22 | ## 📦 安装
23 |
24 | * 安装npm包
25 | ``` bash
26 | yarn add @0xllllh/react-krpano
27 | ```
28 | * 从[Krpano官网](https://krpano.com/download/)下载最新的Krpano并解压得到krpano.js,然后通过script标签引入,使`window.embedpano`函数可用
29 | ```html
30 |
31 | ```
32 |
33 | ## 🔨 使用方法
34 | ### 加载xml
35 | 最基础的用法是通过`Krpano`组件的`xml`参数直接加载krpano xml文件。Krpano组件会忠实的按照xml的配置来进行渲染。
36 |
37 | **krpano.xml**
38 | ```xml
39 |
40 |
41 |
42 |
43 |
44 |
51 |
52 |
53 |
54 | ```
55 |
56 | **App.css**
57 | ``` css
58 | .App {
59 | width: 600px;
60 | height: 400px;
61 | }
62 | ```
63 |
64 | **App.tsx**
65 | ``` tsx
66 | ReactDOM.render(, document.getElementById('app'));
67 | ```
68 |
69 | ### 场景的展示及切换
70 | > 为了简化实现和使用,krpano的image标签的功能被合并到了Scene组件中。通过Scene组件的images属性可以指定场景展示的图片。
71 |
72 | 想要添加一个场景,需要使用Scene组件。
73 | 每个Scene组件代表一个场景,可以通过Krpano组件的`currentScene`来显示与切换当前展示的场景。
74 |
75 | ```tsx
76 | ReactDOM.render(
77 |
78 |
85 |
92 |
93 | ,
94 | document.getElementById('app'));
95 | ```
96 |
97 | ### 热点的使用
98 |
99 | > 目前只支持图片热点
100 |
101 | 使用Hotspot组件可以轻松的渲染热点。同时Hotspot组件还支持一系列的回调设置。
102 |
103 | ```tsx
104 | const App = () => {
105 | const [currentScene, setCurrentScene] = React.useState('scene0');
106 | // 元数据
107 | const scenes = [{
108 | name: 'scene0',
109 | previewUrl: '/preview.jpg',
110 | hotspots: [{
111 | name: 'hot',
112 | type: 'image',
113 | url: 'hotspot.png',
114 | ath: 0,
115 | atv: 20,
116 | onClick: () => setCurrentScene('scene1')
117 | }]
118 | },
119 | name: 'scene1',
120 | previewUrl: '/preview.jpg',
121 | hotspots: []
122 | }]
123 |
124 | return (
125 |
126 |
127 | {scenes.map(sc => (
128 |
129 | {sc.hotspots.map(pt => )}
130 |
131 | ))}
132 |
133 | )
134 | }
135 |
136 | ReactDOM.render(, document.getElementById('app'));
137 | ```
138 |
139 | ### 使用暂未支持的功能
140 | 由于本项目刚开始开发,很多组件和功能都还没完善,如果有需要优先支持的功能可以提issue。倘若急于使用,则可以在获取到`KrpanoActionProxy`后自行调用krpano功能。
141 |
142 | 各种回调函数都会获得KrpanoActionProxy实例作为参数,可以使用其中封装的方法来控制krpano。也可以通过`renderer.krpanoRenderer`获取krpano原生的实例。
143 | ```tsx
144 | const App = () => {
145 | const [currentScene, setCurrentScene] = React.useState('scene0');
146 | const onHotspotClick = React.useCallback((renderer: KrpanoActionProxy) => {
147 | console.log(renderer.get('view.fov'));
148 | setCurrentScene('scene1');
149 | }, []);
150 |
151 | return (
152 | {
156 | console.log('Ready message from App', renderer.krpanoRenderer);
157 | }}
158 | >
159 |
160 |
161 |
169 |
170 |
171 |
172 | );
173 | };
174 | ```
175 |
176 | 此外,对于style和action等标签,可以在写在xml中,而后通过Krpano的`xml`属性引入。xml属性的内容会和React渲染的内容同时存在。
177 | **pano.xml**
178 | ```xml
179 |
180 |
181 | ...
182 |
183 | ```
184 | **App.tsx**
185 | ```tsx
186 | const App = () => (
187 |
192 |
193 |
200 |
201 |
202 | );
203 | ```
204 | ## ❗️ 限制
205 |
206 | * 一个页面同一时间仅展示一个krpano全景图。如果需要同时展示多个全景图,更轻量的方案会比较合适。
207 | * React组件暂时只实现了部分功能。
208 |
209 | ## 🔗 链接
210 | * [Home](https://0xllllh.github.io/react-krpano/)
211 | * [组件参数](https://0xllllh.github.io/react-krpano/#/components)
212 | * [示例项目](https://github.com/0xLLLLH/react-krpano-examples)
213 | * [CHANGELOG](./CHANGELOG.md)
214 | * [Krpano官方文档](https://krpano.com/docu/xml/)
215 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import('@babel/core').ConfigFunction}
3 | */
4 | module.exports = api => {
5 | const isTest = api.env('test');
6 | const isScript = api.env('script');
7 |
8 | return {
9 | presets: [
10 | [
11 | '@babel/preset-env',
12 | {
13 | targets: {
14 | node: 'current',
15 | },
16 | modules: isTest || isScript ? 'auto' : false,
17 | },
18 | ],
19 | '@babel/preset-react',
20 | ['@babel/preset-typescript', { allExtensions: true, isTSX: true }],
21 | ],
22 | plugins: [
23 | [
24 | '@babel/transform-runtime',
25 | {
26 | regenerator: true,
27 | },
28 | ],
29 | [`@babel/plugin-proposal-decorators`, { legacy: true }],
30 | ['@babel/plugin-proposal-class-properties', { loose: true }],
31 | `@babel/plugin-proposal-optional-chaining`,
32 | `@babel/plugin-proposal-object-rest-spread`,
33 | ],
34 | };
35 | };
36 |
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xLLLLH/react-krpano/941d5aa7d092f8bfcff6cf1012f54dbfd410f3f1/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/_navbar.md:
--------------------------------------------------------------------------------
1 | - Translations
2 | - [:us: English](/en-us/)
3 | - [:cn: 中文](/zh-cn/)
4 |
--------------------------------------------------------------------------------
/docs/en-us/_sidebar.md:
--------------------------------------------------------------------------------
1 |
2 | * [Home](en-us/)
3 | * [Components](en-us/components.md)
4 | * [KrpanoActionProxy](en-us/krpano-action-proxy.md)
5 | * [Change Log](en-us/CHANGELOG.md)
6 |
--------------------------------------------------------------------------------
/docs/en-us/components.md:
--------------------------------------------------------------------------------
1 | # Krpano
2 |
3 | The Krpano component is responsible for the initialization of krpano.
4 |
5 | ## Props
6 |
7 | | Attribute | Type | Needed | Description |
8 | |:--|:--:|:--:|:--|
9 | | className | string | | css class of root node |
10 | | currentScene | string | | Name of the currently displayed scene. Must be the same as Scene name attribut |
11 | | target | string | | ID of the Krpano DOM element |
12 | | id | string | | The id of the internal JavaScriptInterfaceObjective, the id of multiple krpano instances needs to be different |
13 | | xml | string | | XML file path to include into krpano instance |
14 | | onReady | EventCallback | | Called after krpano's onready callback is triggered |
15 |
16 | ## Example
17 | **krpano.xml**
18 | ```xml
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | ```
28 |
29 | **App.tsx**
30 | ``` tsx
31 | ReactDOM.render(, document.getElementById('app'));
32 | ```
33 |
34 | # View
35 |
36 | The View component controls and limits the viewing angle.
37 |
38 | ## Attributes
39 |
40 | Currently supports the setting of all attributes, see [official documentation](https://krpano.com/docu/xml/#view) for specific attributes . The following are some of the more commonly used attributes:
41 |
42 | | Attribute | Type | Needed | Description |
43 | |:--|:--:|:--:|:--|
44 | | hlookat | number | | The horizontal coordinate of the current viewing angle corresponding to the horizontal point |
45 | | vlookat | number | | The current angle of view corresponds to the vertical coordinate of the horizontal point |
46 | | fov | number | | Field of view size |
47 | | fovtype | string | | FoV type,can be: 'VFOV', 'HFOV', 'DFOV', 'MFOV' or 'SFOV' |
48 | | fovmin | number | | Minimum FoV value |
49 | | fovmax | number | | Maximum FoV value |
50 |
51 | > ❗️ **Note**
52 | > that most of the attributes in the react-krpano component are written in camelCase, and the corresponding attributes are all lowercase in krpano.
53 |
54 | ## Example
55 |
56 | ``` tsx
57 | ReactDOM.render(
58 |
59 |
60 | ,
61 | document.getElementById('app')
62 | );
63 | ```
64 |
65 | # Scene
66 |
67 | Represent krpano scenes
68 |
69 | | Attribute | Type | Needed | Description |
70 | |:--|:--:|:--:|:--|
71 | | name | string | ✅ | Scene name |
72 | | previewUrl | string | | Preview image url, usually a low-quality image |
73 | | content | string | | Directly specify the xml content of the scene tag, other settings will be ignored after use |
74 | | imageTagAttributes | Record | | Set image tag attributes, see the [official documentation](https://krpano.com/docu/xml/#image) for details |
75 | | images | [SceneImage] or SceneImageWithMultires[] | | Define picture to display in the scene. When length > 1, it will trigger [multires](https://krpano.com/examples/?multires) |
76 |
77 |
78 | ## Interface
79 | ``` tsx
80 | export interface SceneImage {
81 | /** Image type, generally cube */
82 | type: string;
83 | url: string;
84 | }
85 |
86 | export interface SceneImageWithMultires {
87 | /** Image type, generally cube */
88 | type: string;
89 | url: string;
90 | // multires settings
91 | tiledImageWidth: number;
92 | tiledImageHeight: number;
93 | tileSize?: number;
94 | asPreview?: boolean;
95 | }
96 | ```
97 |
98 | ## Examples
99 |
100 | **Single resolution**
101 | ``` tsx
102 | ReactDOM.render(
103 |
104 |
114 |
115 |
116 | ,
117 | document.getElementById('app')
118 | );
119 | ```
120 |
121 | **Multi-resolution**
122 |
123 | > The image used in multires here is obtained from the Alibaba Cloud OSS image processing API, and other methods can also be used to preprocess the generated image
124 |
125 | ``` tsx
126 | ReactDOM.render(
127 |
128 |
155 |
156 |
157 | ,
158 | document.getElementById('app')
159 | );
160 | ```
161 |
162 | # Hotspot
163 |
164 | ## Attributes
165 |
166 | Currently only supports the setting of image hotspots and their basic attributes. See the [official documentation](https://krpano.com/docu/xml/#hotspot) for specific attributes . The following are some of the more commonly used attributes:
167 |
168 |
169 | | Attribute | Type | Needed | Description |
170 | |:--|:--:|:--:|:--|
171 | | name | string | ✅ | Hotspot name |
172 | | url | string | ✅ | Hotspot image path |
173 | | type | string | ✅ | Hotspot type, currently only support `image` |
174 | | ath | number | | Horizontal coordinate |
175 | | atv | number | | Vertical coordinate |
176 | | onClick | EventCallback | | Click event callback |
177 |
178 | > ❗️ **Note**
179 | > that most of the attributes in the react-krpano component are written in camelCase, and the corresponding attributes are all lowercase in krpano.
180 |
181 | ## Example
182 |
183 | ``` tsx
184 | ReactDOM.render(
185 |
186 |
196 | console.log('hotspot click')}
203 | />
204 |
205 |
206 |
207 | ,
208 | document.getElementById('app')
209 | );
210 | ```
211 |
212 | # Events
213 |
214 | The Events component is mainly used for the processing of global events.
215 |
216 | ## Attributes
217 |
218 | | Attribute | Type | Needed | Description |
219 | |:--|:--:|:--:|:--|
220 | | name | string | | Event name,if specified, it will be concidered as a local event. Please refer to the [official documentation](https://krpano.com/docu/xml/#events) for details |
221 | | keep | boolean | | Not working for the moment |
222 | | onClick | EventCallback | | Click callback, note that if there is an onClick element such as a hotspot, it will not be triggered |
223 | | onViewChange | EventCallback | | Triggered when viewing angle changes |
224 |
225 | EventCallback is a unified interface for event callbacks:
226 | ``` typescript
227 | export type EventCallback = (renderer: KrpanoActionProxy) => void;
228 | ```
229 |
230 | > ❗️ **Note**
231 | > that most of the attributes in the react-krpano component are written in camelCase, and the corresponding attributes are all lowercase in krpano.
232 |
233 | ## Example
234 |
235 | ``` tsx
236 | ReactDOM.render(
237 |
238 |
248 |
249 |
250 | {
252 | console.log(renderer.get('view.fov'));
253 | }}
254 | />
255 | ,
256 | document.getElementById('app')
257 | );
258 | ```
259 |
--------------------------------------------------------------------------------
/docs/en-us/krpano-action-proxy.md:
--------------------------------------------------------------------------------
1 | # KrpanoActionProxy
2 |
3 | The [krpano javascript interface object](https://krpano.com/docu/js/#interfaceobject) used to interact with Krpano instance is too basic. It only provides 5 basics functions, which leads to writting a lot of code and handling various details when you use JavaScript to control Krpano.
4 |
5 | `KrpanoActionProxy` is an encapsulation of the native krpano JS object and provides some methods to simplify the control of Krpano.
6 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/docs/zh-cn/_sidebar.md:
--------------------------------------------------------------------------------
1 |
2 | * [Home](zh-cn/)
3 | * [Components](zh-cn/components.md)
4 | * [KrpanoActionProxy](zh-cn/krpano-action-proxy.md)
5 | * [Change Log](zh-cn/CHANGELOG.md)
6 |
--------------------------------------------------------------------------------
/docs/zh-cn/components.md:
--------------------------------------------------------------------------------
1 | # Krpano
2 |
3 | Krpano组件负责krpano的初始化。
4 |
5 | ## 属性
6 |
7 | | 属性名 | 类型 | 必须 | 说明 |
8 | |:--|:--:|:--:|:--|
9 | | className | string | | 根节点的className|
10 | | currentScene | string | | 当前展示的场景名,需要与Scene的name属性对应 |
11 | | target | string | | 组件渲染的dom元素的id,krpano将被渲染到该元素上 |
12 | | id | string | | 内部JavaScriptInterfaceObjective的id,多个krpano实例的id需要不同 |
13 | | xml | string | | xml地址,可以通过该参数直接展示全景图。也是后续渲染的基础。 |
14 | | onReady | EventCallback | | krpano的onready回调触发后调用 |
15 |
16 | ## 示例
17 | **krpano.xml**
18 | ```xml
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | ```
28 |
29 | **App.tsx**
30 | ``` tsx
31 | ReactDOM.render(, document.getElementById('app'));
32 | ```
33 |
34 | # View
35 |
36 | View组件控制和限制视角。
37 |
38 | ## 属性
39 | 目前支持所有属性的设置,具体属性见[官方文档](https://krpano.com/docu/xml/#view)。
40 | 下面是几个比较常用的属性:
41 |
42 | | 属性名 | 类型 | 必须 | 说明 |
43 | |:--|:--:|:--:|:--|
44 | | hlookat | number | | 当前视角对应水平点的水平坐标|
45 | | vlookat | number | | 当前视角对应水平点的垂直坐标|
46 | | fov | number | | 视场角(Field of View)大小 |
47 | | fovtype | string | | fov类型,取值可以为'VFOV'、'HFOV'、'DFOV'、'MFOV'、'SFOV'中的任意一个 |
48 | | fovmin | number | | fov最小值,默认为1 |
49 | | fovmax | number | | fov最大值,默认为179 |
50 |
51 | > ❗️**注意**
52 | > react-krpano组件中绝大多数属性都以camelCase来书写,而对应的属性在krpano中全都是小写的。
53 |
54 | ## 示例
55 |
56 | ``` tsx
57 | ReactDOM.render(
58 |
59 |
60 | ,
61 | document.getElementById('app')
62 | );
63 | ```
64 |
65 | # Scene
66 |
67 | Scene代表场景。
68 |
69 | | 属性名 | 类型 | 必须 | 说明 |
70 | |:--|:--:|:--:|:--|
71 | | name | string | ✅ | 场景的名称|
72 | | previewUrl | string | | 预览图的url,通常是一张低质量的图片 |
73 | | content | string | | 直接指定scene标签的xml内容,使用后会忽略其他设置 |
74 | | imageTagAttributes | Record | | 给scene内image标签设置的属性,详见[image标签官方文档](https://krpano.com/docu/xml/#image) |
75 | | images | [SceneImage] or SceneImageWithMultires[] | | 场景要展示的图片。当长度大于1时,触发[multires](https://krpano.com/examples/?multires) |
76 |
77 |
78 | ## 相关接口
79 | ``` tsx
80 | export interface SceneImage {
81 | /** 图标类型,一般是cube */
82 | type: string;
83 | url: string;
84 | }
85 |
86 | export interface SceneImageWithMultires {
87 | /** 图标类型,一般是cube */
88 | type: string;
89 | url: string;
90 | // multires配置
91 | tiledImageWidth: number;
92 | tiledImageHeight: number;
93 | tileSize?: number;
94 | asPreview?: boolean;
95 | }
96 | ```
97 |
98 | ## 示例
99 |
100 | **单一分辨率**
101 | ``` tsx
102 | ReactDOM.render(
103 |
104 |
114 |
115 |
116 | ,
117 | document.getElementById('app')
118 | );
119 | ```
120 |
121 | **多分辨率(Multi-resolution)**
122 |
123 | > 此处multires所用的图片是阿里云oss图片处理API得到的,也可以使用其他方式预处理生成图片
124 |
125 | ``` tsx
126 | ReactDOM.render(
127 |
128 |
155 |
156 |
157 | ,
158 | document.getElementById('app')
159 | );
160 | ```
161 |
162 | # Hotspot
163 |
164 | ## 属性
165 | 目前仅支持图片热点及其基本属性的设置,具体属性见[官方文档](https://krpano.com/docu/xml/#hotspot)。
166 | 下面是几个比较常用的属性:
167 |
168 | | 属性名 | 类型 | 必须 | 说明 |
169 | |:--|:--:|:--:|:--|
170 | | name | string | ✅ | 热点名称,同一系列的热点可以有相同的名称|
171 | | url | string | ✅ | 热点图片地址|
172 | | type | string | ✅ | 热点类型,目前仅支持'image' |
173 | | ath | number | | 水平坐标 |
174 | | atv | number | | 垂直坐标 |
175 | | onClick | EventCallback | | 点击回调 |
176 |
177 | > ❗️**注意**
178 | > react-krpano组件中绝大多数属性都以camelCase来书写,而对应的属性在krpano中全都是小写的。
179 |
180 | ## 示例
181 |
182 | ``` tsx
183 | ReactDOM.render(
184 |
185 |
195 | console.log('hotspot click')}
202 | />
203 |
204 |
205 |
206 | ,
207 | document.getElementById('app')
208 | );
209 | ```
210 | # Events
211 | Events组件主要用于全局事件的处理。
212 |
213 |
214 | ## 属性
215 | 目前支持除了`keep`外所有属性的设置,具体属性见[官方文档](https://krpano.com/docu/xml/#hotspot)。
216 | 下面是几个比较常用的属性:
217 |
218 | | 属性名 | 类型 | 必须 | 说明 |
219 | |:--|:--:|:--:|:--|
220 | | name | string | | 事件名称。若提供该属性则会被认为是局部事件,详见[官方文档](https://krpano.com/docu/xml/#events)。|
221 | | keep | boolean | | 暂时不生效 |
222 | | onClick | EventCallback | | 点击回调,注意如果热点等有onClick的元素点击不会触发 |
223 | | onViewChange | EventCallback | | 视角变化时触发 |
224 |
225 | 其中EventCallback是事件回调的统一接口:
226 | ``` typescript
227 | export type EventCallback = (renderer: KrpanoActionProxy) => void;
228 | ```
229 |
230 | > ❗️**注意**
231 | > react-krpano组件中绝大多数属性都以camelCase来书写,而对应的属性在krpano中全都是小写的。
232 |
233 | ## 示例
234 |
235 | ``` tsx
236 | ReactDOM.render(
237 |
238 |
248 |
249 |
250 | {
252 | console.log(renderer.get('view.fov'));
253 | }}
254 | />
255 | ,
256 | document.getElementById('app')
257 | );
258 | ```
259 |
--------------------------------------------------------------------------------
/docs/zh-cn/krpano-action-proxy.md:
--------------------------------------------------------------------------------
1 | # KrpanoActionProxy
2 |
3 | Krpano用于和JavaScript交互的[krpano Javascript-Interface object](https://krpano.com/docu/js/#interfaceobject)过于简单,仅提供了5个最基本的函数,导致使用JavaScript控制Krpano时要写大量的krpano action代码并处理各种细节。
4 |
5 | KrpanoActionProxy是对原生对象的封装,提供了一些方法来简化对Krpano控制。
6 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const { defaults } = require('jest-config');
2 |
3 | /**
4 | * @type {import('@jest/types').Config.ProjectConfig}
5 | */
6 | module.exports = {
7 | transformIgnorePatterns: ['/node_modules/(?!@babel/runtime)'],
8 | testURL: 'https://styled.link',
9 |
10 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
11 | testPathIgnorePatterns: ['/node_modules/'],
12 |
13 | moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx'],
14 |
15 | // setupFilesAfterEnv: ['/setupTests.js'],
16 | // mock
17 | moduleNameMapper: {
18 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
19 | '/__mocks__/fileMock.js',
20 | '\\.(css|less|scss)$': '/__mocks__/styleMock.js',
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/krpano-lib/krpano.swf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0xLLLLH/react-krpano/941d5aa7d092f8bfcff6cf1012f54dbfd410f3f1/krpano-lib/krpano.swf
--------------------------------------------------------------------------------
/lint-staged.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '{src,test}/**/*.{ts,tsx}': ['npm run lint'],
3 | };
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@0xllllh/react-krpano",
3 | "version": "0.1.5",
4 | "description": "React bindings for krpano.",
5 | "source": "./src/index.ts",
6 | "main": "./dist/cjs/index.js",
7 | "module": "./dist/esm/index.js",
8 | "types": "./dist/index.d.ts",
9 | "exports": {
10 | "import": "./dist/esm/index.js",
11 | "require": "./dist/cjs/index.js"
12 | },
13 | "files": [
14 | "dist",
15 | "src"
16 | ],
17 | "scripts": {
18 | "docs": "BABEL_ENV=\"script\" babel-node --extensions \".ts\" ./scripts/syncDoc.ts",
19 | "clean": "rm -rf ./dist",
20 | "test": "jest --coverage",
21 | "watch": "concurrently --kill-others \"npm:watch-*\"",
22 | "watch-types": "tsc -w -p ./tsconfig.typing.json",
23 | "watch-bundle": "rollup -c -w",
24 | "watch-test": "jest --coverage --watch",
25 | "prebuild": "npm run clean",
26 | "build": "tsc -p ./tsconfig.typing.json && rollup -c",
27 | "lint": "eslint src --fix --ext .ts,.tsx",
28 | "release": "HUSKY_SKIP_HOOKS=1 standard-version",
29 | "postrelease": "npm run docs",
30 | "release:alpha": "HUSKY_SKIP_HOOKS=1 standard-version --prerelease alpha",
31 | "release:rc": "HUSKY_SKIP_HOOKS=1 standard-version --prerelease rc",
32 | "prepublishOnly": "npm run build"
33 | },
34 | "repository": {
35 | "type": "git",
36 | "url": "git+https://github.com/0xLLLLH/react-krpano.git"
37 | },
38 | "config": {
39 | "commitizen": {
40 | "path": "node_modules/cz-customizable"
41 | }
42 | },
43 | "keywords": [
44 | "react",
45 | "krpano"
46 | ],
47 | "author": "0xLLLLH",
48 | "license": "MIT",
49 | "bugs": {
50 | "url": "https://github.com/0xLLLLH/react-krpano/issues"
51 | },
52 | "homepage": "https://0xllllh.github.io/react-krpano/",
53 | "devDependencies": {
54 | "@babel/core": "^7.12.10",
55 | "@babel/node": "^7.12.10",
56 | "@babel/plugin-proposal-class-properties": "^7.12.1",
57 | "@babel/plugin-proposal-decorators": "^7.12.1",
58 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
59 | "@babel/plugin-proposal-optional-chaining": "^7.12.7",
60 | "@babel/plugin-transform-runtime": "^7.12.10",
61 | "@babel/preset-env": "^7.12.10",
62 | "@babel/preset-react": "^7.12.10",
63 | "@babel/preset-typescript": "^7.12.7",
64 | "@commitlint/cli": "^11.0.0",
65 | "@commitlint/config-conventional": "^11.0.0",
66 | "@rollup/plugin-babel": "^5.2.2",
67 | "@rollup/plugin-commonjs": "^17.0.0",
68 | "@rollup/plugin-node-resolve": "^11.0.0",
69 | "@rollup/plugin-url": "^6.0.0",
70 | "@types/escape-html": "^1.0.0",
71 | "@types/eslint": "^7.2.5",
72 | "@types/glob": "^7.1.3",
73 | "@types/jest": "^26.0.20",
74 | "@types/node": "^14.14.10",
75 | "@types/react": "^17.0.0",
76 | "@types/react-dom": "^17.0.0",
77 | "@typescript-eslint/eslint-plugin": "^4.9.0",
78 | "@typescript-eslint/parser": "^4.9.0",
79 | "babel-jest": "^26.6.3",
80 | "babel-plugin-jsx-control-statements": "^4.1.0",
81 | "commitizen": "^4.2.4",
82 | "commitlint-config-cz": "^0.13.2",
83 | "concurrently": "^7.0.0",
84 | "cz-customizable": "^6.3.0",
85 | "eslint": "^7.14.0",
86 | "eslint-config-prettier": "^6.15.0",
87 | "eslint-plugin-prettier": "^3.1.4",
88 | "eslint-plugin-react-hooks": "^4.2.0",
89 | "glob": "^7.1.6",
90 | "husky": "^4.3.6",
91 | "jest": "^26.6.3",
92 | "lint-staged": "^10.5.3",
93 | "prettier": "^2.2.1",
94 | "rollup": "^2.34.2",
95 | "rollup-plugin-peer-deps-external": "^2.2.4",
96 | "standard-version": "^9.0.0",
97 | "typescript": "^4.1.2"
98 | },
99 | "peerDependencies": {
100 | "react": "^17.0.1",
101 | "react-dom": "^17.0.1"
102 | },
103 | "dependencies": {
104 | "@babel/runtime": "^7.12.5",
105 | "escape-html": "^1.0.3",
106 | "uuid": "^8.3.1"
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from '@rollup/plugin-babel';
2 | import commonjs from '@rollup/plugin-commonjs';
3 | import external from 'rollup-plugin-peer-deps-external';
4 | import { nodeResolve } from '@rollup/plugin-node-resolve';
5 | import url from '@rollup/plugin-url';
6 |
7 | import pkg from './package.json';
8 |
9 | const extensions = ['.js', '.jsx', '.ts', '.tsx'];
10 |
11 | /**
12 | * @type {import('rollup').RollupOptions}
13 | */
14 | export default {
15 | input: 'src/index.ts',
16 | output: [
17 | {
18 | file: pkg.main,
19 | format: 'cjs',
20 | sourcemap: true,
21 | exports: 'auto',
22 | },
23 | {
24 | file: pkg.module,
25 | format: 'es',
26 | sourcemap: true,
27 | exports: 'auto',
28 | },
29 | ],
30 | plugins: [
31 | // typescript(),
32 | external(),
33 | url(),
34 | nodeResolve({
35 | extensions,
36 | }),
37 | babel({
38 | extensions,
39 | exclude: 'node_modules/**',
40 | babelHelpers: 'runtime',
41 | }),
42 | commonjs({
43 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
44 | }),
45 | ],
46 | external: [/@babel\/runtime/, 'react', 'react-dom'],
47 | };
48 |
--------------------------------------------------------------------------------
/scripts/syncDoc.ts:
--------------------------------------------------------------------------------
1 | import glob from 'glob';
2 | import fs from 'fs';
3 | import p from 'path';
4 |
5 | const docPath = './docs';
6 |
7 | function syncMDFiles() {
8 | glob('./!(CHANGELOG|README)*.md', (err, files) => {
9 | files.forEach(path => {
10 | const target = p.join(docPath, path.replace('./', '/'));
11 | console.log(`Moving doc from ${path} to ${target}`);
12 | const fileAsString = fs.readFileSync(path, { encoding: 'utf8' }).replace(/\]\(\.\//g, '](/');
13 | fs.writeFileSync(target, fileAsString);
14 | });
15 | });
16 | }
17 |
18 | function main() {
19 | syncMDFiles();
20 | }
21 |
22 | main();
23 |
--------------------------------------------------------------------------------
/src/KrpanoActionProxy.ts:
--------------------------------------------------------------------------------
1 | import { NativeKrpanoRendererObject } from './types';
2 | import { buildKrpanoAction, buildKrpanoTagSetterActions } from './utils';
3 |
4 | export type HandlerFunc = (renderer: KrpanoActionProxy) => void;
5 |
6 | interface EventHandler {
7 | eventName: string;
8 | selector: string;
9 | handler: HandlerFunc;
10 | }
11 | export class KrpanoActionProxy {
12 | name: string;
13 | krpanoRenderer?: NativeKrpanoRendererObject;
14 | eventHandlers: EventHandler[] = [];
15 |
16 | constructor(krpanoRenderer?: NativeKrpanoRendererObject, name = 'ReactKrpanoActionProxy') {
17 | this.krpanoRenderer = krpanoRenderer;
18 | this.name = name;
19 | }
20 |
21 | call(action: string, nexttick = false): void {
22 | const actionStr = nexttick ? `nexttick(${action})` : action;
23 |
24 | this.krpanoRenderer?.call(actionStr);
25 | }
26 |
27 | set(name: string, ...params: Array): void {
28 | this.call(buildKrpanoAction('set', name, ...params));
29 | }
30 |
31 | setTag(
32 | tag: 'scene' | 'hotspot' | 'layer' | 'view' | 'events',
33 | name: string | null,
34 | attrs: Record
35 | ): void {
36 | let nexttick = false;
37 |
38 | if (tag === 'hotspot' || tag === 'events') {
39 | nexttick = true;
40 | }
41 |
42 | this.call(buildKrpanoTagSetterActions(name ? `${tag}[${name}]` : tag, attrs), nexttick);
43 | }
44 |
45 | get(name: string): T {
46 | return this.krpanoRenderer?.get(name);
47 | }
48 |
49 | loadScene(name: string): void {
50 | this.call(buildKrpanoAction('loadscene', name, 'null', 'MERGE', 'BLEND(0.5)'));
51 | }
52 | removeScene(name: string): void {
53 | if (this.get('scene') && typeof this.get('scene').removeItem === 'function') {
54 | this.get('scene').removeItem(name);
55 | } else {
56 | // TODO: report Error
57 | }
58 | }
59 |
60 | addHotspot(name: string, attrs: Record): void {
61 | this.call(buildKrpanoAction('addhotspot', name), true);
62 | this.setTag('hotspot', name, attrs);
63 | }
64 | removeHotspot(name: string): void {
65 | this.call(buildKrpanoAction('removehotspot', name), true);
66 | }
67 |
68 | on(eventName: string, selector: string, handler: HandlerFunc): this {
69 | this.eventHandlers.push({
70 | eventName: eventName.toLowerCase(),
71 | selector,
72 | handler,
73 | });
74 | return this;
75 | }
76 |
77 | off(eventName: string, selector: string, handler: HandlerFunc): void {
78 | this.eventHandlers = this.eventHandlers.filter(
79 | e => !(e.eventName === eventName.toLowerCase() && e.selector === selector && e.handler === handler)
80 | );
81 | }
82 |
83 | fire(eventName: string, selector: string): void {
84 | this.eventHandlers
85 | .filter(e => e.eventName === eventName.toLowerCase() && e.selector === selector)
86 | .map(({ handler }) => handler(this));
87 | }
88 |
89 | bindEvents(selector: string, mapEventsToHandler: Record): void {
90 | Object.keys(mapEventsToHandler).map(eventName => {
91 | const func = mapEventsToHandler[eventName];
92 |
93 | if (func) {
94 | this.on(eventName, selector, func);
95 | }
96 | });
97 | }
98 |
99 | unbindEvents(selector: string, mapEventsToHandler: Record): void {
100 | Object.keys(mapEventsToHandler).map(eventName => {
101 | const func = mapEventsToHandler[eventName];
102 |
103 | if (func) {
104 | this.off(eventName, selector, func);
105 | }
106 | });
107 | }
108 | }
109 |
110 | export default KrpanoActionProxy;
111 |
--------------------------------------------------------------------------------
/src/__snapshots__/utils.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`buildKrpanoAction() call 1`] = `"set(view.fov, 120);"`;
4 |
5 | exports[`buildKrpanoAction() call 2`] = `"addhotspot(hotspotName);"`;
6 |
7 | exports[`buildKrpanoAction() call 3`] = `"loadscene(scene1, null, MERGE, BLEND(0.5));"`;
8 |
9 | exports[`buildKrpanoTagSetterActions() assignstyle 1`] = `"set(hotspot[hotspotName].ath, 10);set(hotspot[hotspotName].atv, 20);assignstyle(hotspot[hotspotName], hotspot_style1);"`;
10 |
11 | exports[`buildKrpanoTagSetterActions() call with tag name 1`] = `"set(scene[scene1].autoload, false);set(scene[scene1].content, '');"`;
12 |
13 | exports[`buildKrpanoTagSetterActions() call with tag name 2`] = `"set(hotspot[hotspotName].ath, 10);set(hotspot[hotspotName].atv, 20);"`;
14 |
15 | exports[`buildKrpanoTagSetterActions() call without tag name 1`] = `"set(view.fov, 90);"`;
16 |
17 | exports[`buildKrpanoTagSetterActions() escape string 1`] = `"set(hotspot[hotspotName].needescape, \\"<div>It's normal string</div>\\");set(hotspot[hotspotName].content, \\"It's xml string, keep everything\\");"`;
18 |
19 | exports[`buildXML() children empty array 1`] = `""`;
20 |
21 | exports[`buildXML() empty attrs & children 1`] = `""`;
22 |
23 | exports[`buildXML() empty children 1`] = `""`;
24 |
25 | exports[`buildXML() one childreny 1`] = `""`;
26 |
--------------------------------------------------------------------------------
/src/components/Events.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { KrpanoRendererContext } from '../contexts/KrpanoRendererContext';
3 | import { EventCallback } from '../types';
4 | import { mapEventPropsToJSCall } from '../utils';
5 |
6 | export interface EventsConfig {
7 | /** 事件名,若存在该参数则为局部事件 */
8 | name?: string;
9 | /** 暂时不支持 */
10 | keep?: boolean;
11 | onEnterFullscreen?: EventCallback;
12 | onExitFullscreen?: EventCallback;
13 | onXmlComplete?: EventCallback;
14 | onPreviewComplete?: EventCallback;
15 | onLoadComplete?: EventCallback;
16 | onBlendComplete?: EventCallback;
17 | onNewPano?: EventCallback;
18 | onRemovePano?: EventCallback;
19 | onNewScene?: EventCallback;
20 | onXmlError?: EventCallback;
21 | onLoadError?: EventCallback;
22 | onKeydown?: EventCallback;
23 | onKeyup?: EventCallback;
24 | onClick?: EventCallback;
25 | onSingleClick?: EventCallback;
26 | onDoubleClick?: EventCallback;
27 | onMousedown?: EventCallback;
28 | onMouseup?: EventCallback;
29 | onMousewheel?: EventCallback;
30 | onContextmenu?: EventCallback;
31 | onIdle?: EventCallback;
32 | onViewChange?: EventCallback;
33 | onViewChanged?: EventCallback;
34 | onResize?: EventCallback;
35 | onFrameBufferResize?: EventCallback;
36 | onAutoRotateStart?: EventCallback;
37 | onAutoRotateStop?: EventCallback;
38 | onAutoRotateOneRound?: EventCallback;
39 | onAutoRotateChange?: EventCallback;
40 | onIPhoneFullscreen?: EventCallback;
41 | }
42 |
43 | export interface EventsProps extends EventsConfig {}
44 |
45 | const GlobalEvents = '__GlobalEvents';
46 |
47 | export const Events: React.FC = ({ name, keep, children, ...EventsAttrs }) => {
48 | const renderer = React.useContext(KrpanoRendererContext);
49 | const EventSelector = React.useMemo(() => `events[${name || GlobalEvents}]`, [name]);
50 |
51 | // 在renderer上绑定回调
52 | React.useEffect(() => {
53 | renderer?.bindEvents(EventSelector, { ...EventsAttrs });
54 |
55 | return () => {
56 | renderer?.unbindEvents(EventSelector, { ...EventsAttrs });
57 | };
58 | }, [renderer, EventsAttrs, EventSelector]);
59 |
60 | // Krpano标签上添加js call,触发事件
61 | React.useEffect(() => {
62 | renderer?.setTag(
63 | 'events',
64 | // 全局事件直接设置
65 | name || null,
66 | mapEventPropsToJSCall(
67 | { ...EventsAttrs },
68 | eventName => `js(${renderer.name}.fire(${eventName},${EventSelector}))`
69 | )
70 | );
71 | // eslint-disable-next-line react-hooks/exhaustive-deps
72 | }, [EventSelector, name, renderer]);
73 |
74 | return ;
75 | };
76 |
77 | export default Events;
78 |
--------------------------------------------------------------------------------
/src/components/Hotspot.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { KrpanoRendererContext } from '../contexts/KrpanoRendererContext';
3 | import { EventCallback } from '../types';
4 | import { Logger, mapEventPropsToJSCall, mapObject } from '../utils';
5 |
6 | export interface ImageHotspotConfig {
7 | name: string;
8 | url?: string;
9 | type?: string;
10 | keep?: boolean;
11 | visible?: boolean;
12 | enabled?: boolean;
13 | handCursor?: boolean;
14 | cursor?: string;
15 | maskChildren?: boolean;
16 | zOrder?: string;
17 | style?: string;
18 | ath?: number;
19 | atv?: number;
20 | edge?: string;
21 | zoom?: boolean;
22 | distorted?: boolean;
23 | rx?: number;
24 | ry?: number;
25 | rz?: number;
26 | width?: string;
27 | height?: string;
28 | scale?: number;
29 | rotate?: number;
30 | alpha?: number;
31 | onOver?: EventCallback;
32 | onHover?: EventCallback;
33 | onOut?: EventCallback;
34 | onDown?: EventCallback;
35 | onUp?: EventCallback;
36 | onClick?: EventCallback;
37 | onLoaded?: EventCallback;
38 | }
39 |
40 | export interface HotspotProps extends ImageHotspotConfig {}
41 |
42 | export const Hotspot: React.FC = ({ name, children, ...hotspotAttrs }) => {
43 | const renderer = useContext(KrpanoRendererContext);
44 | const EventSelector = `hotspot[${name}]`;
45 |
46 | React.useEffect(() => {
47 | const eventsObj = mapObject({ ...hotspotAttrs }, (key, value) => {
48 | if (key.startsWith('on') && typeof value === 'function') {
49 | return {
50 | [key]: value,
51 | };
52 | }
53 | return {};
54 | });
55 | renderer?.bindEvents(EventSelector, eventsObj as any);
56 |
57 | renderer?.addHotspot(name, {});
58 |
59 | return () => {
60 | renderer?.unbindEvents(EventSelector, eventsObj as any);
61 | renderer?.removeHotspot(name);
62 | };
63 | // eslint-disable-next-line react-hooks/exhaustive-deps
64 | }, []);
65 |
66 | React.useEffect(() => {
67 | renderer?.setTag(
68 | 'hotspot',
69 | name,
70 | Object.assign(
71 | { ...hotspotAttrs },
72 | mapEventPropsToJSCall({ ...hotspotAttrs }, key => `js(${renderer?.name}.fire(${key},${EventSelector}))`)
73 | )
74 | );
75 | Logger.log(`hotspot ${name} updated due to attrs change`);
76 | }, [renderer, name, hotspotAttrs, EventSelector]);
77 |
78 | return {children}
;
79 | };
80 |
81 | export default Hotspot;
82 |
--------------------------------------------------------------------------------
/src/components/Krpano.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CurrentSceneContext } from '../contexts/CurrentSceneContext';
3 | import { KrpanoRendererContext } from '../contexts/KrpanoRendererContext';
4 | import useEventCallback from '../hooks/useEventCallback';
5 | import { useKrpano } from '../hooks/useKrpano';
6 | import KrpanoActionProxy from '../KrpanoActionProxy';
7 | import { NativeKrpanoRendererObject } from '../types';
8 | import { Logger } from '../utils';
9 |
10 | export interface KrpanoProps {
11 | className?: string;
12 | currentScene?: string;
13 | /** Krpano XML地址 */
14 | xml?: string;
15 | target?: string;
16 | id?: string;
17 | onReady?: (renderer: KrpanoActionProxy) => void;
18 | enableLogger?: boolean;
19 | }
20 |
21 | export const Krpano: React.FC = ({
22 | className,
23 | currentScene,
24 | target = 'krpano',
25 | id,
26 | xml,
27 | onReady,
28 | children,
29 | enableLogger = false,
30 | }) => {
31 | const [renderer, setRenderer] = React.useState(null);
32 | const onReadyRef = useEventCallback(onReady);
33 | const onReadyCallback = React.useCallback(
34 | (obj: NativeKrpanoRendererObject) => {
35 | const renderer = new KrpanoActionProxy(obj);
36 | (window as any)[renderer.name] = renderer;
37 | setRenderer(renderer);
38 | Logger.enabled = enableLogger;
39 | Logger.log('Renderer ready.');
40 |
41 | if (onReadyRef.current) {
42 | onReadyRef.current(renderer);
43 | }
44 | },
45 | [enableLogger, onReadyRef]
46 | );
47 | const krpanoConfig = React.useMemo(
48 | () => ({
49 | target,
50 | id,
51 | xml: xml || null,
52 | onready: onReadyCallback,
53 | }),
54 | [target, id, xml, onReadyCallback]
55 | );
56 |
57 | useKrpano(krpanoConfig);
58 |
59 | return (
60 |
61 |
62 |
63 | {renderer ? children : null}
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default Krpano;
71 |
--------------------------------------------------------------------------------
/src/components/Scene.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { CurrentSceneContext } from '../contexts/CurrentSceneContext';
3 | import { KrpanoRendererContext } from '../contexts/KrpanoRendererContext';
4 | import { buildXML, Logger, XMLMeta } from '../utils';
5 |
6 | export interface SceneImage {
7 | type: string;
8 | url: string;
9 | }
10 |
11 | export interface SceneImageWithMultires {
12 | type: string;
13 | url: string;
14 | // multires props
15 | tiledImageWidth: number;
16 | tiledImageHeight: number;
17 | tileSize?: number;
18 | asPreview?: boolean;
19 | }
20 |
21 | export interface SceneProps {
22 | name: string;
23 | previewUrl?: string;
24 | /** 直接指定scene的xml内容。指定后会忽略其他设置 */
25 | content?: string;
26 | /** image标签的附加属性,仅少部分情况用到 */
27 | imageTagAttributes?: Record;
28 | /** Scene包含的图片。数组的长度大于1时按multires解析为多个level */
29 | images?: [SceneImage] | SceneImageWithMultires[];
30 | }
31 |
32 | export const Scene: React.FC = ({
33 | name,
34 | previewUrl,
35 | imageTagAttributes = {},
36 | images = [],
37 | content,
38 | children,
39 | }) => {
40 | const renderer = useContext(KrpanoRendererContext);
41 | const currentScene = useContext(CurrentSceneContext);
42 |
43 | React.useEffect(() => {
44 | const contentImageMeta: XMLMeta = {
45 | tag: 'image',
46 | attrs: imageTagAttributes,
47 | children: [],
48 | };
49 |
50 | // multires
51 | if (images.length > 1) {
52 | contentImageMeta.children!.push(
53 | ...(images as SceneImageWithMultires[]).map(
54 | ({ tiledImageWidth, tiledImageHeight, tileSize, asPreview = false, type, ...img }) => {
55 | const imgXML: XMLMeta = {
56 | tag: 'level',
57 | // FIXME: tiledImageWidth等值使用者不一定会提供,需要进行检查、提示以及fallback
58 | attrs: {
59 | tiledImageWidth,
60 | tiledImageHeight,
61 | asPreview,
62 | },
63 | children: [
64 | {
65 | tag: type,
66 | attrs: { ...img },
67 | },
68 | ],
69 | };
70 |
71 | if (tileSize) {
72 | imgXML.attrs = Object.assign(imgXML.attrs, { tileSize });
73 | }
74 |
75 | return imgXML;
76 | }
77 | )
78 | );
79 | } else if (images.length === 1) {
80 | const { type, ...img } = images[0] as SceneImage;
81 |
82 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
83 | contentImageMeta.children!.push({
84 | tag: type,
85 | attrs: { ...img },
86 | });
87 | }
88 |
89 | renderer?.setTag('scene', name, {
90 | content:
91 | content ||
92 | `${previewUrl ? `` : ''}${
93 | images.length > 0 ? buildXML(contentImageMeta) : ''
94 | }`,
95 | });
96 |
97 | return () => {
98 | renderer?.removeScene(name);
99 | };
100 | }, [renderer, name, images, imageTagAttributes, content, previewUrl]);
101 |
102 | React.useEffect(() => {
103 | if (currentScene === name) {
104 | renderer?.loadScene(name);
105 | Logger.log(`Scene ${name} loaded due to currentScene change.`);
106 | }
107 | }, [name, renderer, currentScene]);
108 |
109 | return {currentScene === name ? children : null}
;
110 | };
111 |
112 | export default Scene;
113 |
--------------------------------------------------------------------------------
/src/components/View.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { KrpanoRendererContext } from '../contexts/KrpanoRendererContext';
3 |
4 | /**
5 | * @see https://krpano.com/docu/xml/#view
6 | */
7 | export interface ViewProps {
8 | hlookat?: number;
9 | vlookat?: number;
10 | fov?: number;
11 | fovMin?: number;
12 | fovMax?: number;
13 | camRoll?: number;
14 | /**
15 | * @see https://krpano.com/docu/xml/#view.fovtype
16 | */
17 | fovType?: 'VFOV' | 'HFOV' | 'DFOV' | 'MFOV' | 'SFOV';
18 | maxPixelZoom?: number;
19 | mFovRatio?: number;
20 | distortion?: number;
21 | distortionFovLink?: number;
22 | stereographic?: boolean;
23 | pannini?: number;
24 | architectural?: number;
25 | architecturalOnlyMiddle?: boolean;
26 | /**
27 | * @see https://krpano.com/docu/xml/#view.limitview
28 | */
29 | limitView?: 'off' | 'auto' | 'lookat' | 'range' | 'fullrange' | 'offrange';
30 | hlookatMin?: number;
31 | hlookatMax?: number;
32 | vlookatMin?: number;
33 | vlookatMax?: number;
34 | rx?: number;
35 | ry?: number;
36 | tx?: number;
37 | ty?: number;
38 | tz?: number;
39 | ox?: number;
40 | oy?: number;
41 | oz?: number;
42 | children?: null;
43 | }
44 |
45 | export const View: React.FC = ({ children, ...viewAttrs }) => {
46 | const renderer = useContext(KrpanoRendererContext);
47 |
48 | React.useEffect(() => {
49 | renderer?.setTag('view', null, { ...viewAttrs });
50 | }, [renderer, viewAttrs]);
51 |
52 | return {children}
;
53 | };
54 |
55 | export default View;
56 |
--------------------------------------------------------------------------------
/src/contexts/CurrentSceneContext.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const CurrentSceneContext = React.createContext(null);
4 |
--------------------------------------------------------------------------------
/src/contexts/KrpanoRendererContext.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import KrpanoActionProxy from '../KrpanoActionProxy';
3 |
4 | export const KrpanoRendererContext = React.createContext(null);
5 |
--------------------------------------------------------------------------------
/src/hooks/useEventCallback.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | /**
4 | * This hook is design to cache component's callback from props, and avoid unnecessary re-render cause by arrow function.
5 | * This pattern might cause problems in the concurrent mode.
6 | *
7 | * [related section in react doc](https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback)
8 | *
9 | * [related github ticket](https://github.com/facebook/react/issues/14099#issuecomment-440013892)
10 | *
11 | * @param callback callback to memorize
12 | * @returns mutable ref object
13 | */
14 | export default function useEventCallback(callback: T): React.MutableRefObject {
15 | const callbackRef = React.useRef(callback);
16 |
17 | React.useLayoutEffect(() => {
18 | callbackRef.current = callback;
19 | }, [callback]);
20 |
21 | return callbackRef;
22 | }
23 |
--------------------------------------------------------------------------------
/src/hooks/useKrpano.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IKrpanoConfig } from '../types';
3 |
4 | export const useKrpano = (config: IKrpanoConfig): void => {
5 | React.useEffect(() => {
6 | const defaultConfig: Partial = {
7 | html5: 'prefer',
8 | xml: null,
9 | consolelog: process.env.NODE_ENV === 'development',
10 | };
11 | const embedpano = () => {
12 | if (typeof window.embedpano === 'function') window.embedpano(Object.assign({}, defaultConfig, config));
13 | };
14 |
15 | if (typeof window.embedpano === 'function') {
16 | embedpano();
17 | } else {
18 | // TODO: install krpano
19 | throw new Error('Krpano required');
20 | }
21 | }, [config]);
22 | };
23 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './components/Krpano';
2 | export * from './components/View';
3 | export * from './components/Scene';
4 | export * from './components/Hotspot';
5 | export * from './components/Events';
6 |
7 | export * from './KrpanoActionProxy';
8 | export * from './types';
9 | export * from './contexts/KrpanoRendererContext';
10 | export * from './hooks/useKrpano';
11 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import KrpanoActionProxy from './KrpanoActionProxy';
2 |
3 | /**
4 | * @see https://krpano.com/docu/html/#wmode
5 | */
6 | export interface IKrpanoConfig {
7 | /**
8 | * 全景图xml路径。需要手动设置为null才不会加载。
9 | * @see https://krpano.com/docu/html/#xml
10 | */
11 | xml?: string | null;
12 | /** 挂载点id */
13 | target: string;
14 | swf?: string;
15 | id?: string;
16 | bgcolor?: string;
17 | /**
18 | * @see https://krpano.com/docu/html/#html5
19 | */
20 | html5?: string;
21 | flash?: string;
22 | wmode?: string;
23 | localfallback?: string;
24 | vars?: Record;
25 | initvars?: Record;
26 | consolelog?: boolean;
27 | basepath?: string;
28 | mwheel?: boolean;
29 | capturetouch?: boolean;
30 | focus?: boolean;
31 | webglsettings?: Record;
32 | webxr?: string;
33 | mobilescale?: number;
34 | touchdevicemousesupport?: boolean;
35 | fakedevice?: string;
36 | onready?: (renderer: NativeKrpanoRendererObject) => void;
37 | }
38 |
39 | export interface NativeKrpanoRendererObject {
40 | get(key: string): any;
41 | call(action: string): void;
42 | }
43 |
44 | export type EventCallback = (renderer: KrpanoActionProxy) => void;
45 |
46 | declare global {
47 | interface Window {
48 | embedpano?: (config: IKrpanoConfig) => void;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { buildKrpanoAction, buildKrpanoTagSetterActions, buildXML, mapObject, mapEventPropsToJSCall } from './utils';
2 |
3 | describe('buildKrpanoAction()', () => {
4 | test('call', () => {
5 | expect(buildKrpanoAction('set', 'view.fov', 120)).toMatchSnapshot();
6 |
7 | expect(buildKrpanoAction('addhotspot', 'hotspotName')).toMatchSnapshot();
8 |
9 | expect(buildKrpanoAction('loadscene', 'scene1', 'null', 'MERGE', 'BLEND(0.5)')).toMatchSnapshot();
10 | });
11 | });
12 |
13 | describe('buildKrpanoTagSetterActions()', () => {
14 | test('call without tag name', () => {
15 | expect(buildKrpanoTagSetterActions('view', { fov: 90 })).toMatchSnapshot();
16 | });
17 | test('call with tag name', () => {
18 | expect(
19 | buildKrpanoTagSetterActions('scene[scene1]', { autoload: false, content: '' })
20 | ).toMatchSnapshot();
21 |
22 | expect(
23 | buildKrpanoTagSetterActions('hotspot[hotspotName]', {
24 | ath: 10,
25 | atv: 20,
26 | c: undefined,
27 | })
28 | ).toMatchSnapshot();
29 | });
30 |
31 | test('assignstyle', () => {
32 | expect(
33 | buildKrpanoTagSetterActions('hotspot[hotspotName]', {
34 | ath: 10,
35 | atv: 20,
36 | style: 'hotspot_style1',
37 | })
38 | ).toMatchSnapshot();
39 | });
40 |
41 | test('escape string', () => {
42 | expect(
43 | buildKrpanoTagSetterActions('hotspot[hotspotName]', {
44 | needEscape: `It's normal string
`,
45 | content: `It's xml string, keep everything`,
46 | })
47 | ).toMatchSnapshot();
48 | });
49 | });
50 |
51 | describe('buildXML()', () => {
52 | test('empty attrs & children', () => {
53 | expect(
54 | buildXML({
55 | tag: 'tagName',
56 | attrs: {},
57 | })
58 | ).toMatchSnapshot();
59 | });
60 |
61 | test('empty children', () => {
62 | expect(
63 | buildXML({
64 | tag: 'tagName',
65 | attrs: {
66 | key1: 'val1',
67 | key2: 2,
68 | },
69 | })
70 | ).toMatchSnapshot();
71 | });
72 |
73 | test('children empty array', () => {
74 | expect(
75 | buildXML({
76 | tag: 'tagName',
77 | attrs: {
78 | key1: 'val1',
79 | key2: 2,
80 | },
81 | children: [],
82 | })
83 | ).toMatchSnapshot();
84 | });
85 |
86 | test('one childreny', () => {
87 | expect(
88 | buildXML({
89 | tag: 'tagName',
90 | attrs: {
91 | key1: 'val1',
92 | key2: 2,
93 | },
94 | children: [
95 | {
96 | tag: 'inner',
97 | attrs: {
98 | k: 'v',
99 | },
100 | },
101 | ],
102 | })
103 | ).toMatchSnapshot();
104 | });
105 | });
106 |
107 | describe('mapObject()', () => {
108 | test('call', () => {
109 | const obj = { a: 1, b: 2 };
110 |
111 | expect(mapObject(obj, (k, v) => ({ [k]: (v as number) + 1 }))).toEqual({
112 | a: 2,
113 | b: 3,
114 | });
115 | });
116 | });
117 |
118 | describe('mapEventPropsToJSCall()', () => {
119 | test('call', () => {
120 | const obj = { a: 1, b: 2, onClick: () => console.log(1) };
121 |
122 | expect(mapEventPropsToJSCall(obj, k => `js(${k}, selector)`)).toEqual({
123 | onClick: 'js(onClick, selector)',
124 | });
125 | });
126 | });
127 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import escapeHTML from 'escape-html';
2 |
3 | type FuncName = 'set' | 'loadxml' | 'loadscene' | 'addhotspot' | 'removehotspot' | 'nexttick';
4 |
5 | /**
6 | * 执行单个函数
7 | * @param func 函数名
8 | * @param params 参数列表
9 | */
10 | export const buildKrpanoAction = (func: FuncName, ...params: Array): string =>
11 | `${func}(${params.map(p => `${p}`).join(', ')});`;
12 |
13 | /**
14 | * 动态添加标签用
15 | * @see https://krpano.com/forum/wbb/index.php?page=Thread&threadID=15873
16 | */
17 | export const buildKrpanoTagSetterActions = (
18 | name: string,
19 | attrs: Record
20 | ): string =>
21 | Object.keys(attrs)
22 | .map(key => {
23 | const val = attrs[key];
24 | key = key.toLowerCase();
25 | if (val === undefined) {
26 | return '';
27 | }
28 | // 如果属性值中有双引号,需要改用单引号
29 | let quote = '"';
30 | if (val.toString().includes(quote)) {
31 | // eslint-disable-next-line quotes
32 | quote = "'";
33 | }
34 | if (key === 'style') {
35 | return `assignstyle(${name}, ${val});`;
36 | }
37 | if (typeof val === 'boolean' || typeof val === 'number') {
38 | return `set(${name}.${key}, ${val});`;
39 | }
40 | // content是XML文本,不能转义,因为不涉及用户输入也不需要
41 | return `set(${name}.${key}, ${quote}${key === 'content' ? val : escapeHTML(val.toString())}${quote});`;
42 | })
43 | .filter(str => !!str)
44 | .join('');
45 |
46 | export const Logger = {
47 | enabled: false,
48 | log: (...args: any[]): void => {
49 | /* istanbul ignore next */
50 | if (Logger.enabled && process.env.NODE_ENV === 'development') {
51 | console.log(...args);
52 | }
53 | },
54 | };
55 |
56 | export interface XMLMeta {
57 | tag: string;
58 | attrs: Record;
59 | children?: XMLMeta[];
60 | }
61 |
62 | /**
63 | * 根据元数据构建xml
64 | */
65 | export const buildXML = ({ tag, attrs, children }: XMLMeta): string => {
66 | const attributes = Object.keys(attrs)
67 | .map(key => `${key.toLowerCase()}="${attrs[key]}"`)
68 | .join(' ');
69 |
70 | if (children && children.length) {
71 | return `<${tag} ${attributes}>${children.map(child => buildXML(child)).join('')}${tag}>`;
72 | }
73 | return `<${tag} ${attributes} />`;
74 | };
75 |
76 | /**
77 | * 对Object进行map操作
78 | */
79 | export const mapObject = (
80 | obj: Record,
81 | mapper: (key: string, value: unknown) => Record
82 | ): Record => {
83 | return Object.assign(
84 | {},
85 | ...Object.keys(obj).map(key => {
86 | const value = obj[key];
87 | return mapper(key, value);
88 | })
89 | );
90 | };
91 |
92 | /**
93 | * 主要用于绑定Krpano事件和js调用。提取某个对象中的onXXX属性并替换为对应的调用字符串,丢弃其他属性
94 | */
95 | export const mapEventPropsToJSCall = (
96 | obj: Record,
97 | getString: (key: string, value: unknown) => string
98 | ): Record =>
99 | mapObject(obj, (key, value) => {
100 | if (key.startsWith('on') && typeof value === 'function') {
101 | return { [key]: getString(key, value) };
102 | }
103 | return {};
104 | }) as Record;
105 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "CommonJS",
4 | "target": "ESNext",
5 | "lib": ["dom", "esnext"],
6 | "moduleResolution": "node",
7 | "jsx": "react",
8 | "sourceMap": true,
9 | "esModuleInterop": true,
10 | "noImplicitReturns": true,
11 | "noImplicitThis": true,
12 | "noImplicitAny": true,
13 | "strictNullChecks": true,
14 | "suppressImplicitAnyIndexErrors": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "allowSyntheticDefaultImports": true,
18 | "importHelpers": true,
19 | "noEmitHelpers": true,
20 | "noEmit": true,
21 | "experimentalDecorators": true,
22 | "skipLibCheck": true
23 | },
24 | "include": ["src"],
25 | "exclude": ["node_modules", "dist", "example"]
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.typing.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "noEmit": false,
5 | "declaration": true,
6 | "emitDeclarationOnly": true,
7 | "declarationDir": "dist"
8 | },
9 | "exclude": ["node_modules", "dist", "example"]
10 | }
11 |
--------------------------------------------------------------------------------