├── _config.yml
├── src
├── const
│ └── PanoConst.js
├── effect
│ ├── style
│ │ ├── images
│ │ │ ├── close.png
│ │ │ ├── hands.png
│ │ │ ├── phone.png
│ │ │ ├── video-hands.png
│ │ │ └── video-phone-gyro.png
│ │ ├── EffectAlphaVideoPanel.less
│ │ ├── EffectInfoCard.less
│ │ ├── EffectControlPanel.less
│ │ ├── EffectVideoPanel.less
│ │ └── EffectImageCard.less
│ ├── EffectControlPanel.jsx
│ ├── EffectInfoCard.jsx
│ ├── EffectImageCard.jsx
│ ├── EffectVideoPanel.jsx
│ ├── EffectContainer.jsx
│ ├── EffectAlphaVideoPanle.jsx
│ └── NewEffectAlphaVideoPanle.jsx
├── event
│ ├── Events.js
│ ├── EventCenter.js
│ └── EventBus.js
├── reducer.js
├── setupTests.js
├── App.test.js
├── store.js
├── index.css
├── index.js
├── App.css
├── utils
│ ├── osuitls.js
│ └── fullscreen.js
├── display
│ ├── loader
│ │ ├── ObjLoader.js
│ │ └── FBXLoader.js
│ ├── ResourceBox
│ │ ├── EmbeddedResource
│ │ │ ├── EmbeddedImageBox.js
│ │ │ ├── EmbeddedVideoBox.js
│ │ │ ├── EmbeddedTextBox.js
│ │ │ ├── EmbeddedBox.js
│ │ │ └── EmbeddedBoxManager.js
│ │ └── ResourceBoxHelper.js
│ ├── CenterModelHelper.js
│ ├── SpriteParticleHelper.js
│ └── HotSpotHelper.js
├── action
│ ├── CameraMoveAction.js
│ └── ViewConvertHelper.js
├── redux
│ └── player.redux.js
├── logo.svg
├── controls
│ └── DeviceOrientationControls.js
├── Player.js
├── manager
│ └── VRHelper.js
└── texture
│ └── TextureHelper.js
├── public
├── favicon.ico
├── logo192.png
├── logo512.png
├── robots.txt
├── shuttle.mp4
├── texture1.png
├── SambaDancing.fbx
├── hotspot_video.png
├── manifest.json
├── index.html
└── mock
│ └── view1.json
├── deploy
├── upload.sh
└── nginx.conf
├── doc
├── imgs
│ └── react-xrplayer-preview.png
├── feature-and-todo.md
├── dev.md
└── api-doc.md
├── .npmignore
├── .babelrc
├── Dockerfile
├── config
├── jest
│ ├── cssTransform.js
│ └── fileTransform.js
├── pnpTs.js
├── getHttpsConfig.js
├── paths.js
├── env.js
├── modules.js
└── webpackDevServer.config.js
├── example
├── src
│ └── index.js
└── index.html
├── .gitignore
├── LICENSE
├── jsdoc.json
├── scripts
├── test.js
├── start.js
└── build.js
├── README.md
├── package.json
└── .vscode
└── reactjsx.code-snippets
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/src/const/PanoConst.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 全景球体的半径
3 | */
4 | export const Radius = 500;
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/shuttle.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/public/shuttle.mp4
--------------------------------------------------------------------------------
/public/texture1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/public/texture1.png
--------------------------------------------------------------------------------
/deploy/upload.sh:
--------------------------------------------------------------------------------
1 | # only runnable on ljc pc
2 |
3 | scp -r build aliyunecs:/production/front/youmu-xr-client
--------------------------------------------------------------------------------
/public/SambaDancing.fbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/public/SambaDancing.fbx
--------------------------------------------------------------------------------
/public/hotspot_video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/public/hotspot_video.png
--------------------------------------------------------------------------------
/doc/imgs/react-xrplayer-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/doc/imgs/react-xrplayer-preview.png
--------------------------------------------------------------------------------
/src/effect/style/images/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/src/effect/style/images/close.png
--------------------------------------------------------------------------------
/src/effect/style/images/hands.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/src/effect/style/images/hands.png
--------------------------------------------------------------------------------
/src/effect/style/images/phone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/src/effect/style/images/phone.png
--------------------------------------------------------------------------------
/src/effect/style/images/video-hands.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/src/effect/style/images/video-hands.png
--------------------------------------------------------------------------------
/src/effect/style/images/video-phone-gyro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ZWboy97/react-xrplayer/HEAD/src/effect/style/images/video-phone-gyro.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # .npmignore
2 | src
3 | .babelrc
4 | .gitignore
5 | deploy
6 | .vscode
7 | build
8 | config
9 | node_modules
10 | public
11 | scripts
12 | Dockerfile
13 | yarn.lock
14 | /jsdoc
--------------------------------------------------------------------------------
/src/event/Events.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 事件
3 | */
4 |
5 | const EVENT_SENCE_RES_READY = 'event_sence_res_ready'; //全景背景资源准备完成
6 |
7 | export default {
8 | EVENT_SENCE_RES_READY
9 | };
10 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "@babel/plugin-proposal-class-properties"
8 | ]
9 | }
--------------------------------------------------------------------------------
/src/reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { player } from './redux/player.redux';
3 | const rootReducer = combineReducers({
4 | player
5 | });
6 | export default rootReducer;
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # production environment
2 | FROM nginx
3 | COPY ./build /usr/share/nginx/html
4 | RUN rm /etc/nginx/conf.d/default.conf
5 | COPY deploy/nginx.conf /etc/nginx/conf.d
6 | EXPOSE 80
7 | CMD ["nginx", "-g", "daemon off;" ]
--------------------------------------------------------------------------------
/src/event/EventCenter.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * 尝试将事件处理统一管理起来
4 | */
5 | class EventCenter {
6 | constructor() {
7 | this.init();
8 | }
9 |
10 | init = () => {
11 | }
12 |
13 | }
14 | export default EventCenter;
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/deploy/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 |
4 | location / {
5 | root /usr/share/nginx/html;
6 | index index.html index.htm;
7 | try_files $uri $uri/ /index.html;
8 | }
9 |
10 | error_page 500 502 503 504 /50x.html;
11 |
12 | location = /50x.html {
13 | root /usr/share/nginx/html;
14 | }
15 | }
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render();
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This is a custom Jest transformer turning style imports into empty objects.
4 | // http://facebook.github.io/jest/docs/en/webpack.html
5 |
6 | module.exports = {
7 | process() {
8 | return 'module.exports = {};';
9 | },
10 | getCacheKey() {
11 | // The output is always the same.
12 | return 'cssTransform';
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk'; // 中间件,用来处理异步数据
3 | import rootReducer from './reducer';
4 |
5 | const store = createStore(
6 | rootReducer,
7 | compose(
8 | applyMiddleware(thunk),
9 | window.devToolsExtension ? window.devToolsExtension() : f => f
10 | )
11 | );
12 | export default store;
13 |
--------------------------------------------------------------------------------
/example/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './app';
4 |
5 |
6 | ReactDOM.render(
7 | ,
8 | document.getElementById('root'));
9 |
10 | // If you want your app to work offline and load faster, you can change
11 | // unregister() to register() below. Note this comes with some pitfalls.
12 | // Learn more about service workers: https://bit.ly/CRA-PWA
13 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | #IDE
4 | .idea
5 |
6 | # dependencies
7 | /node_modules
8 | /.pnp
9 | .pnp.js
10 |
11 | # testing
12 | /coverage
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | yarn.lock
25 | package-lock.json
26 | /lib
27 | /build
28 | /jsdoc
29 | /public/res
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './index.css';
3 | import Player from './Player';
4 | import { Provider } from 'react-redux' // https://react-redux.js.org/api/connect
5 | import store from './store';
6 |
7 | class XRPlayer extends React.Component {
8 | render() {
9 | return (
10 |
11 |
12 |
13 | )
14 | }
15 | }
16 |
17 | export default XRPlayer;
18 |
19 |
--------------------------------------------------------------------------------
/src/effect/style/EffectAlphaVideoPanel.less:
--------------------------------------------------------------------------------
1 | .alpha_video_overlay {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | }
10 |
11 | .grep_overlay {
12 | background-color: rgba(0, 0, 0, .5);
13 | }
14 |
15 | .close {
16 | position: absolute;
17 | z-index: 12;
18 | right: 1rem;
19 | top: 1rem;
20 | width: 2rem;
21 | cursor: pointer;
22 | height: 2rem;
23 | background: url(./images/close.png) 0 0 no-repeat;
24 | background-size: cover;
25 | }
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/config/pnpTs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { resolveModuleName } = require('ts-pnp');
4 |
5 | exports.resolveModuleName = (
6 | typescript,
7 | moduleName,
8 | containingFile,
9 | compilerOptions,
10 | resolutionHost
11 | ) => {
12 | return resolveModuleName(
13 | moduleName,
14 | containingFile,
15 | compilerOptions,
16 | resolutionHost,
17 | typescript.resolveModuleName
18 | );
19 | };
20 |
21 | exports.resolveTypeReferenceDirective = (
22 | typescript,
23 | moduleName,
24 | containingFile,
25 | compilerOptions,
26 | resolutionHost
27 | ) => {
28 | return resolveModuleName(
29 | moduleName,
30 | containingFile,
31 | compilerOptions,
32 | resolutionHost,
33 | typescript.resolveTypeReferenceDirective
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/effect/style/EffectInfoCard.less:
--------------------------------------------------------------------------------
1 | .info-overlay {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background-color: rgba(0, 0, 0, .5);
10 | }
11 |
12 | .iframe {
13 | min-width: 500px;
14 | width: 50%;
15 | position: absolute;
16 | top: 50%;
17 | left: 50%;
18 | height: 70%;
19 | z-index: 12;
20 | -webkit-transform: translateX(-50%) translateY(-50%);
21 | -ms-transform: translateX(-50%) translateY(-50%);
22 | transform: translateX(-50%) translateY(-50%);
23 | }
24 |
25 | .info-close {
26 | position: absolute;
27 | z-index: 12;
28 | right: 2rem;
29 | top: 2rem;
30 | width: 2rem;
31 | height: 2rem;
32 | background: url(./images/close.png) 0 0 no-repeat;
33 | background-size: cover;
34 | }
--------------------------------------------------------------------------------
/src/effect/style/EffectControlPanel.less:
--------------------------------------------------------------------------------
1 | .control-overlay {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | background-color: rgba(0, 0, 0, .5);
8 | }
9 |
10 | .control-container {
11 | position: absolute;
12 | top: 50%;
13 | left: 50%;
14 | width: 42rem;
15 | min-height: 20%;
16 | font-size: 1rem;
17 | background-color: rgba(0, 0, 0, .3);
18 | z-index: 12;
19 | visibility: visible;
20 | -webkit-transform: translateX(-50%) translateY(-50%);
21 | -ms-transform: translateX(-50%) translateY(-50%);
22 | transform: translateX(-50%) translateY(-50%);
23 | }
24 |
25 |
26 | .control-close {
27 | position: absolute;
28 | z-index: 12;
29 | right: 1rem;
30 | top: 1rem;
31 | width: 2rem;
32 | cursor: pointer;
33 | height: 2rem;
34 | background: url(./images/close.png) 0 0 no-repeat;
35 | background-size: cover;
36 | }
--------------------------------------------------------------------------------
/src/effect/style/EffectVideoPanel.less:
--------------------------------------------------------------------------------
1 | .video-overlay {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background-color: rgba(0, 0, 0, .5);
10 | }
11 |
12 | .container {
13 |
14 | .video {
15 | position: absolute;
16 | top: 50%;
17 | left: 50%;
18 | width: 60%;
19 | max-height: 100%;
20 | z-index: 12;
21 | -webkit-transform: translateX(-50%) translateY(-50%);
22 | -ms-transform: translateX(-50%) translateY(-50%);
23 | transform: translateX(-50%) translateY(-50%);
24 | }
25 | }
26 |
27 | .content {
28 | margin: auto;
29 | }
30 |
31 | .video-close {
32 | position: absolute;
33 | z-index: 12;
34 | right: 1rem;
35 | top: 1rem;
36 | width: 2rem;
37 | cursor: pointer;
38 | height: 2rem;
39 | background: url(./images/close.png) 0 0 no-repeat;
40 | background-size: cover;
41 | }
--------------------------------------------------------------------------------
/src/utils/osuitls.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-useless-escape */
2 | export var OS = {
3 | weixin: navigator.userAgent.indexOf('MicroMessenger') > -1,
4 | android: /android/i.test(navigator.userAgent.toLowerCase()),
5 | ios: /(iphone|ipad|ipod|ios)/i.test(navigator.userAgent.toLowerCase()),
6 | googlePixel: navigator.userAgent.match(/;\sPixel\sBuild\//),
7 | MiOS: navigator.userAgent.match(/;\sMI\s\d\sBuild\//),
8 | samsungOS: navigator.userAgent.match(/;\sSM\-\w+\sBuild\//),
9 | isGooglePixel: function () {
10 | return this.googlePixel != null;
11 | },
12 | isMiOS: function () {
13 | return this.MiOS != null;
14 | },
15 | isSamsung: function () {
16 | return this.samsungOS != null;
17 | },
18 | isMobile: function () {
19 | return this.android || this.ios;
20 | },
21 |
22 | isAndroid: function () {
23 | return this.android;
24 | },
25 |
26 | isiOS: function () {
27 | return this.ios;
28 | },
29 |
30 | isWeixin: function () {
31 | return this.weixin;
32 | }
33 | }
--------------------------------------------------------------------------------
/src/effect/EffectControlPanel.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import './style/EffectControlPanel.less';
4 |
5 | class EffectControlPanel extends Component {
6 |
7 | componentDidMount() {
8 | }
9 |
10 | onCloseClickListener = (e) => {
11 | e.preventDefault();
12 | if (this.props.onCloseClickHandler) {
13 | this.props.onCloseClickHandler();
14 | }
15 | }
16 |
17 | componentWillUnmount() {
18 | }
19 |
20 | render() {
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
31 |
32 | )
33 | }
34 | }
35 |
36 | EffectControlPanel.propTypes = {
37 | onCloseClickHandler: PropTypes.func.isRequired,
38 | };
39 |
40 | export default EffectControlPanel;
41 |
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 ZWboy
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 |
--------------------------------------------------------------------------------
/src/effect/style/EffectImageCard.less:
--------------------------------------------------------------------------------
1 | .img-overlay {
2 | position: absolute;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | width: 100%;
8 | height: 100%;
9 | background-color: rgba(0, 0, 0, .5);
10 | }
11 |
12 | .container {
13 | .image {
14 | cursor: pointer;
15 | position: absolute;
16 | top: 50%;
17 | left: 50%;
18 | width: 60%;
19 | max-height: 100%;
20 | z-index: 12;
21 | -webkit-transform: translateX(-50%) translateY(-50%);
22 | -ms-transform: translateX(-50%) translateY(-50%);
23 | transform: translateX(-50%) translateY(-50%);
24 | // -webkit-transform: translateX(-50%) translateY(-50%);
25 | // -ms-transform: translateX(-50%) translateY(-50%);
26 | // transform: translateX(-50%) translateY(-50%);
27 | }
28 | }
29 |
30 | .img-close {
31 | position: absolute;
32 | z-index: 12;
33 | right: 1rem;
34 | top: 1rem;
35 | width: 2rem;
36 | cursor: pointer;
37 | height: 2rem;
38 | background: url(./images/close.png) 0 0 no-repeat;
39 | background-size: cover;
40 | }
--------------------------------------------------------------------------------
/src/effect/EffectInfoCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Iframe from 'react-iframe'
4 | import './style/EffectInfoCard.less';
5 |
6 | class EffectInfoCard extends Component {
7 |
8 | onCloseClickListener = (e) => {
9 | e.preventDefault();
10 | if (this.props.onCloseClickHandler) {
11 | this.props.onCloseClickHandler();
12 | }
13 | }
14 |
15 | render() {
16 | return (
17 | )
30 | }
31 | }
32 |
33 | EffectInfoCard.propTypes = {
34 | onCloseClickHandler: PropTypes.func.isRequired,
35 | iframeUrl: PropTypes.string.isRequired
36 | };
37 |
38 | export default EffectInfoCard;
39 |
40 |
--------------------------------------------------------------------------------
/src/display/loader/ObjLoader.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 |
3 | class ObjLoader {
4 |
5 | constructor(scene) {
6 | this.scene = scene;
7 | this.obj = null;
8 | this.data = null;
9 | }
10 |
11 | loadObj = (data) => {
12 | this.data = data;
13 | var loader = new THREE.ObjectLoader();
14 | loader.load(data.objUrl,
15 | (obj) => {
16 | this.obj = obj;
17 | var texture = new THREE.TextureLoader().load(data.texture);
18 | obj.material = new THREE.MeshBasicMaterial({
19 | map: texture,
20 | });
21 | this.scene.add(obj);
22 | obj.scale.set(data.scale, data.scale, data.scale);//网格模型缩放
23 | obj.geometry.center();//几何体居中
24 | },
25 | (data) => { console.log('loading json obj', data.loaded); },
26 | (e) => { console.log('err', e); }
27 | )
28 | }
29 |
30 | display = () => {
31 | this.scene.add(this.obj);
32 | }
33 |
34 | remove = () => {
35 | this.scene.remove(this.obj);
36 | this.mixer = null;
37 | this.animationAction = null;
38 | }
39 |
40 | update = () => {
41 |
42 | }
43 | }
44 |
45 | export default ObjLoader;
--------------------------------------------------------------------------------
/jsdoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tags": {
3 | "allowUnknownTags": true,
4 | "dictionaries": [
5 | "jsdoc",
6 | "closure"
7 | ]
8 | },
9 | "source": {
10 | "include": [
11 | "src/manager",
12 | "src/display"
13 | ],
14 | "includePattern": ".js$",
15 | "exclude": []
16 | },
17 | "opts": {
18 | "encoding": "utf8",
19 | "destination": "./jsdoc/",
20 | "readme": "./README.md",
21 | "recurse": true,
22 | "verbose": true,
23 | "template": "node_modules/clean-jsdoc-theme",
24 | "theme_opts": {
25 | "title": "react-xrplayer",
26 | "menu": [
27 | {
28 | "title": "Website",
29 | "link": "http://blog.zwboy.cn/react-xrplayer/",
30 | "target": "_blank",
31 | "class": "some-class",
32 | "id": "some-id"
33 | },
34 | {
35 | "title": "Github",
36 | "link": "https://github.com/ZWboy97/react-xrplayer",
37 | "target": "_blank",
38 | "class": "some-class",
39 | "id": "some-id"
40 | }
41 | ]
42 | }
43 | },
44 | "plugins": [
45 | "plugins/markdown"
46 | ]
47 | }
--------------------------------------------------------------------------------
/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const camelcase = require('camelcase');
5 |
6 | // This is a custom Jest transformer turning file imports into filenames.
7 | // http://facebook.github.io/jest/docs/en/webpack.html
8 |
9 | module.exports = {
10 | process(src, filename) {
11 | const assetFilename = JSON.stringify(path.basename(filename));
12 |
13 | if (filename.match(/\.svg$/)) {
14 | // Based on how SVGR generates a component name:
15 | // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
16 | const pascalCaseFilename = camelcase(path.parse(filename).name, {
17 | pascalCase: true,
18 | });
19 | const componentName = `Svg${pascalCaseFilename}`;
20 | return `const React = require('react');
21 | module.exports = {
22 | __esModule: true,
23 | default: ${assetFilename},
24 | ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
25 | return {
26 | $$typeof: Symbol.for('react.element'),
27 | type: 'svg',
28 | ref: ref,
29 | key: null,
30 | props: Object.assign({}, props, {
31 | children: ${assetFilename}
32 | })
33 | };
34 | }),
35 | };`;
36 | }
37 |
38 | return `module.exports = ${assetFilename};`;
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/src/effect/EffectImageCard.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import './style/EffectImageCard.less';
4 |
5 | class EffectImageCard extends Component {
6 |
7 | onCloseClickListener = (e) => {
8 | e.preventDefault();
9 | if (this.props.onCloseClickHandler) {
10 | this.props.onCloseClickHandler();
11 | }
12 | }
13 |
14 | onImageClickListenr = (e) => {
15 | e.preventDefault();
16 | if (this.props.jumpUrl) {
17 | window.open(this.props.jumpUrl, '_blank');
18 | }
19 | }
20 |
21 | render() {
22 | return (
23 |
24 |
25 |

30 |
31 |
35 |
36 | )
37 | }
38 | }
39 |
40 | EffectImageCard.propTypes = {
41 | onCloseClickHandler: PropTypes.func.isRequired,
42 | imageUrl: PropTypes.string.isRequired,
43 | jumpUrl: PropTypes.string,
44 | };
45 |
46 | export default EffectImageCard;
47 |
48 |
--------------------------------------------------------------------------------
/scripts/test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Do this as the first thing so that any code reading it knows the right env.
4 | process.env.BABEL_ENV = 'test';
5 | process.env.NODE_ENV = 'test';
6 | process.env.PUBLIC_URL = '';
7 |
8 | // Makes the script crash on unhandled rejections instead of silently
9 | // ignoring them. In the future, promise rejections that are not handled will
10 | // terminate the Node.js process with a non-zero exit code.
11 | process.on('unhandledRejection', err => {
12 | throw err;
13 | });
14 |
15 | // Ensure environment variables are read.
16 | require('../config/env');
17 |
18 |
19 | const jest = require('jest');
20 | const execSync = require('child_process').execSync;
21 | let argv = process.argv.slice(2);
22 |
23 | function isInGitRepository() {
24 | try {
25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
26 | return true;
27 | } catch (e) {
28 | return false;
29 | }
30 | }
31 |
32 | function isInMercurialRepository() {
33 | try {
34 | execSync('hg --cwd . root', { stdio: 'ignore' });
35 | return true;
36 | } catch (e) {
37 | return false;
38 | }
39 | }
40 |
41 | // Watch unless on CI or explicitly running all tests
42 | if (
43 | !process.env.CI &&
44 | argv.indexOf('--watchAll') === -1 &&
45 | argv.indexOf('--watchAll=false') === -1
46 | ) {
47 | // https://github.com/facebook/create-react-app/issues/5210
48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository();
49 | argv.push(hasSourceControl ? '--watch' : '--watchAll');
50 | }
51 |
52 |
53 | jest.run(argv);
54 |
--------------------------------------------------------------------------------
/src/action/CameraMoveAction.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 控制全景相机的入场和出场
3 | */
4 | import TWEEN from '@tweenjs/tween.js';
5 |
6 |
7 | class CameraMoveAction {
8 |
9 | constructor(start, end, duration = 5000, delay = 1000) {
10 | this.tween = null;
11 | this.onCompleteHandler = null;
12 | this.onStartHandler = null;
13 | this.onUpdateHandler = null;
14 | this.init(start, end, duration, delay);
15 | }
16 |
17 | init = (start, end, duration, delay) => {
18 | let coords = start;
19 | this.tween = new TWEEN.Tween(coords)
20 | .to({
21 | lat: end.lat,
22 | lon: end.lon,
23 | fov: end.fov,
24 | distance: end.distance
25 | }, duration)
26 | .delay(delay)
27 | .easing(TWEEN.Easing.Quadratic.InOut)
28 | .onUpdate(() => {
29 | let pos = {
30 | lat: coords.lat,
31 | lon: coords.lon,
32 | fov: coords.fov,
33 | distance: coords.distance
34 | }
35 | this.onUpdateHandler && this.onUpdateHandler(pos);
36 | })
37 | .onStart(() => {
38 | this.onStartHandler && this.onStartHandler();
39 | })
40 | .onComplete(() => {
41 | this.onCompleteHandler && this.onCompleteHandler();
42 | });
43 | }
44 |
45 | start = () => {
46 | if (this.tween) {
47 | this.tween.start();
48 | }
49 | }
50 | }
51 |
52 | export default CameraMoveAction;
--------------------------------------------------------------------------------
/src/display/ResourceBox/EmbeddedResource/EmbeddedImageBox.js:
--------------------------------------------------------------------------------
1 | import EmbeddedBox from "./EmbeddedBox";
2 |
3 | class EmbeddedImageBox extends EmbeddedBox{
4 | constructor(id) {
5 | super(id, 'image');
6 | this.url = '';
7 | this.width = 30;
8 | this.height = 30
9 | this.showTypeChangable = true;
10 | this.update();
11 | }
12 |
13 | setImage = (url, width = 30, height = 30) => {
14 | this.url = url;
15 | this.width = width;
16 | this.height = height;
17 |
18 | this.update();
19 | }
20 |
21 | setImageSize = (width, height) => {
22 | this.width = width;
23 | this.height = height;
24 | this.update();
25 | }
26 |
27 | getImageInfo = () => {
28 | return {url: this.url, width: this.width, height: this.height};
29 | }
30 |
31 | //内部控制
32 | update = () => {
33 | if (this.url === '') return;
34 | this.meshReady = false;
35 | let img = document.createElement("img");
36 | img.src = this.url;
37 | this.canvas = document.createElement('canvas');
38 | this.initCanvas();
39 | this.canvas.width = this.width;
40 | this.canvas.height = this.height;
41 | let context = this.canvas.getContext('2d');
42 | let width = this.width;
43 | let height = this.height;
44 | let createPlane = this.createPlane;
45 | let tHis = this;
46 | img.onload = function () {
47 | context.drawImage(img, 0, 0, width, height);
48 | createPlane();
49 | tHis.manager && tHis.manager.updateDisplay(tHis);
50 | }
51 | }
52 | }
53 |
54 | export default EmbeddedImageBox;
--------------------------------------------------------------------------------
/src/action/ViewConvertHelper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 实现小行星和普通视野等效果的切换
3 | */
4 |
5 | import CameraMoveAction from './CameraMoveAction';
6 |
7 | class ViewConvertHelper {
8 |
9 | constructor(camera, controls) {
10 | this.camera = camera;
11 | this.state = null;
12 | this.controls = controls;
13 | this.cameraMoveAction = null;
14 | this.onCompleteHandler = null;
15 | this.onStartHandler = null;
16 | }
17 |
18 | toNormalView = (durtime = 8000, delay = 0) => {
19 | if (this.state && this.state === 'normal') return;
20 | this.cameraMoveAction = new CameraMoveAction(this.camera,
21 | { x: 0, y: 0, z: 100, fov: 80 }, durtime, delay);
22 | this.cameraMoveAction.onStartHandler = () => {
23 | this.controls && this.controls.disConnect();
24 | }
25 | this.cameraMoveAction.onCompleteHandler = () => {
26 | this.controls && this.controls.connect();
27 | this.state = 'planet';
28 | }
29 | this.cameraMoveAction.start();
30 | }
31 |
32 | toPlanetView = (durtime = 8000, delay = 0) => {
33 | if (this.state && this.state === 'planet') return;
34 | this.cameraMoveAction = new CameraMoveAction(this.camera,
35 | { x: 0, y: 450, z: 0, fov: 150 }, durtime, delay);
36 | this.cameraMoveAction.onStartHandler = () => {
37 | this.controls && this.controls.disConnect();
38 | }
39 | this.cameraMoveAction.onCompleteHandler = () => {
40 | this.controls && this.controls.connect();
41 | this.state = 'planet';
42 | }
43 | this.cameraMoveAction.start();
44 | }
45 |
46 | }
47 |
48 | export default ViewConvertHelper;
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
25 | 全景视频互动直播
26 |
27 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
25 | 全景视频互动直播
26 |
27 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/display/CenterModelHelper.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 支持向全景场景中心加入可以360°观看的3D模型
3 | */
4 | import FBXLoader from './loader/FBXLoader';
5 | import ObjLoader from './loader/ObjLoader';
6 |
7 | class CenterModelHelper {
8 |
9 | constructor(scene) {
10 | this.scene = scene;
11 | this.modelLoaderMap = new Map();
12 | }
13 |
14 | loadModel = (model_key, model) => {
15 | const promise = new Promise((resolve, reject) => {
16 | const loader = this.getLoader(model);
17 | this.modelLoaderMap.set(model_key, loader);
18 | loader.loadObj(model);
19 | resolve();
20 | }).catch((reason) => {
21 | })
22 | promise.then();
23 | }
24 |
25 | loadModelList = (model_list) => {
26 | model_list && model_list.forEach(data => {
27 | this.loadModel(data[0], data[1])
28 | })
29 | }
30 |
31 | removeModel = (model_key) => {
32 | const modelLoader = this.modelLoaderMap.get(model_key);
33 | modelLoader && modelLoader.remove();
34 | this.modelLoaderMap.delete(model_key);
35 | }
36 |
37 | removeAllModel = () => {
38 | const loaderArray = Array.from(this.modelLoaderMap.values());
39 | loaderArray.forEach(loader => {
40 | loader.remove();
41 | });
42 | this.modelLoaderMap.clear();
43 | }
44 |
45 | getLoader = (data) => {
46 | switch (data.modeFormat) {
47 | case 'fbx':
48 | return new FBXLoader(this.scene);
49 | case 'obj':
50 | return new ObjLoader(this.scene);
51 | default: return null;
52 | }
53 | }
54 |
55 | update = () => {
56 | this.modelLoaderMap.forEach(data => {
57 | const loader = data;
58 | loader && loader.update();
59 | })
60 | }
61 | }
62 |
63 | export default CenterModelHelper;
--------------------------------------------------------------------------------
/src/display/loader/FBXLoader.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import FBXLoader from 'three-fbxloader-offical';
3 |
4 |
5 | class MyFBXLoader {
6 |
7 | constructor(scene) {
8 | this.scene = scene;
9 | this.mixer = null; //混合器
10 | this.obj = null;
11 | this.animationAction = null;
12 | this.ambient = null;
13 | this.clock = new THREE.Clock();
14 | }
15 |
16 | loadObj = (data) => {
17 | var loader = new FBXLoader(); //创建一个FBX加载器
18 | loader.load(data.objUrl,
19 | (obj) => {
20 | this.obj = obj;
21 | this.display();
22 | console.log('load fbxobj success:', this.obj);
23 | this.obj.translateY(-80);
24 | this.startAnimation();
25 | },
26 | (data) => {
27 | console.log('load fbx obj', data.loaded);
28 | },
29 | (e) => {
30 | console.log('load fbxobj error:', e);
31 | });
32 | }
33 |
34 | display = () => {
35 | this.scene.add(this.obj);
36 | this.ambient = new THREE.AmbientLight(0xffffff);
37 | this.scene.add(this.ambient);
38 | }
39 |
40 | remove = () => {
41 | this.scene.remove(this.obj);
42 | this.scene.remove(this.ambient);
43 | this.mixer = null;
44 | this.animationAction = null;
45 | }
46 |
47 | startAnimation = () => {
48 | this.mixer = new THREE.AnimationMixer(this.obj);
49 | this.animationAction = this.mixer.clipAction(this.obj.animations[0]);
50 | this.animationAction.play();
51 | }
52 |
53 | stopAnimation = () => {
54 | this.animationAction && this.animationAction.stop();
55 | }
56 |
57 | update = () => {
58 | if (this.mixer) {
59 | const delta = this.clock.getDelta() //方法获得两帧的时间间隔
60 | this.mixer.update(delta);
61 | }
62 | }
63 |
64 | }
65 |
66 | export default MyFBXLoader;
--------------------------------------------------------------------------------
/src/redux/player.redux.js:
--------------------------------------------------------------------------------
1 | const ENABLE_EFFECT_CONTAINER = "ENABLE_EFFECT_CONTAINER";
2 | const EFFECT_DATA = "EFFECT_DATA";
3 | const VOLUME = "VOLUME";
4 | const MUTED = "MUTED";
5 |
6 | // 初始state中的数据
7 | const initialState = {
8 | is_effect_displaying: false,
9 | effect_data: {},
10 | volume: 0.9,
11 | muted: true
12 | }
13 |
14 | //reducer, 根据action对state进行处理,返回新的state
15 | export function player(state = initialState, action) {
16 | switch (action.type) {
17 | case ENABLE_EFFECT_CONTAINER:
18 | return { ...state, is_effect_displaying: action.payload };
19 | case EFFECT_DATA:
20 | return { ...state, effect_data: action.payload };
21 | case VOLUME:
22 | return { ...state, volume: action.payload };
23 | case MUTED:
24 | return { ...state, muted: action.payload }
25 | default:
26 | return state;
27 | }
28 | }
29 |
30 | //actionCreator, 创建action对象
31 | function enableEffectContainerAction(data) {
32 | return {
33 | payload: data,
34 | type: ENABLE_EFFECT_CONTAINER
35 | }
36 | }
37 |
38 | function effectDataAction(data) {
39 | return {
40 | payload: data,
41 | type: EFFECT_DATA
42 | }
43 | }
44 |
45 | function volumeAction(data) {
46 | return {
47 | payload: data,
48 | type: VOLUME
49 | }
50 | }
51 |
52 | function mutedAction(data) {
53 | return {
54 | payload: data,
55 | type: MUTED
56 | }
57 | }
58 |
59 | //在组件中调用的dispatch action的函数
60 | export function enableEffectContainer(enable) {
61 | return dispatch => {
62 | dispatch(enableEffectContainerAction(enable))
63 | }
64 | }
65 |
66 | export function setEffectData(data) {
67 | return dispatch => {
68 | dispatch(effectDataAction(data));
69 | }
70 | }
71 |
72 | export function setGlobalVolume(value) {
73 | return dispatch => {
74 | dispatch(volumeAction(value));
75 | }
76 | }
77 |
78 | export function setGlobalMuted(muted) {
79 | return dispatch => {
80 | dispatch(mutedAction(muted));
81 | }
82 | }
83 |
84 |
--------------------------------------------------------------------------------
/config/getHttpsConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const crypto = require('crypto');
6 | const chalk = require('react-dev-utils/chalk');
7 | const paths = require('./paths');
8 |
9 | // Ensure the certificate and key provided are valid and if not
10 | // throw an easy to debug error
11 | function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
12 | let encrypted;
13 | try {
14 | // publicEncrypt will throw an error with an invalid cert
15 | encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
16 | } catch (err) {
17 | throw new Error(
18 | `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
19 | );
20 | }
21 |
22 | try {
23 | // privateDecrypt will throw an error with an invalid key
24 | crypto.privateDecrypt(key, encrypted);
25 | } catch (err) {
26 | throw new Error(
27 | `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
28 | err.message
29 | }`
30 | );
31 | }
32 | }
33 |
34 | // Read file and throw an error if it doesn't exist
35 | function readEnvFile(file, type) {
36 | if (!fs.existsSync(file)) {
37 | throw new Error(
38 | `You specified ${chalk.cyan(
39 | type
40 | )} in your env, but the file "${chalk.yellow(file)}" can't be found.`
41 | );
42 | }
43 | return fs.readFileSync(file);
44 | }
45 |
46 | // Get the https config
47 | // Return cert files if provided in env, otherwise just true or false
48 | function getHttpsConfig() {
49 | const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
50 | const isHttps = HTTPS === 'true';
51 |
52 | if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
53 | const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
54 | const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
55 | const config = {
56 | cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
57 | key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
58 | };
59 |
60 | validateKeyAndCerts({ ...config, keyFile, crtFile });
61 | return config;
62 | }
63 | return isHttps;
64 | }
65 |
66 | module.exports = getHttpsConfig;
67 |
--------------------------------------------------------------------------------
/src/display/ResourceBox/EmbeddedResource/EmbeddedVideoBox.js:
--------------------------------------------------------------------------------
1 | import EmbeddedBox from "./EmbeddedBox";
2 | import * as THREE from "three";
3 |
4 | class EmbeddedVideoBox extends EmbeddedBox{
5 | constructor(id) {
6 | super(id, 'video');
7 | this.url = '';
8 | this.width = 30;
9 | this.height = 30;
10 | this.autoplay = false;
11 | this.poster = '';
12 |
13 | this.update();
14 | }
15 |
16 | setVideo = (url, width = 30, height = 30) => {
17 | this.url = url;
18 | this.width = width;
19 | this.height = height;
20 |
21 | this.update();
22 | }
23 |
24 | setVideoSize = (width, height) => {
25 | this.width = width;
26 | this.height = height;
27 |
28 | this.update();
29 | }
30 |
31 | setEnableAutoDisplay = (enable) => {
32 | this.autoplay = enable;
33 | this.play();
34 | }
35 |
36 | play = () => {
37 | this.videoElement && this.videoElement.play();
38 | }
39 |
40 | pause = () => {
41 | this.videoElement && this.videoElement.pause();
42 | }
43 |
44 | //内部控制
45 | newPlaneMaterial = () => {
46 | this.videoElement && this.kill();
47 | this.videoElement = document.createElement("video");
48 | this.videoElement.src = this.url;
49 | if (this.autoplay === true)
50 | this.videoElement.autoplay = 'autoplay';
51 | let texture = new THREE.VideoTexture(this.videoElement);
52 | texture.minFilter = THREE.LinearFilter;
53 | texture.magFilter = THREE.LinearFilter;
54 | texture.format = THREE.RGBFormat;
55 | let planeMaterial = new THREE.MeshBasicMaterial({map: texture});
56 | planeMaterial.depthTest = this.depthTest;
57 | planeMaterial.needsUpdate = true;
58 | planeMaterial.map.needsUpdate = true;
59 | planeMaterial.transparent = true;
60 | planeMaterial.opacity = 1;
61 |
62 | return planeMaterial;
63 | }
64 |
65 | update = () => {
66 | this.createPlane();
67 | this.manager && this.manager.updateDisplay(this);
68 | }
69 |
70 | kill = () => {
71 | this.videoElement.src = '';
72 | }
73 | }
74 |
75 | export default EmbeddedVideoBox;
--------------------------------------------------------------------------------
/src/utils/fullscreen.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import fscreen from "fscreen";
4 |
5 | class FullScreen extends Component {
6 | static propTypes = {
7 | children: PropTypes.node.isRequired,
8 | enabled: PropTypes.bool.isRequired,
9 | onChange: PropTypes.func,
10 | };
11 |
12 | static defaultProps = {
13 | enabled: false,
14 | };
15 |
16 | constructor(props) {
17 | super(props);
18 |
19 | this.detectFullScreen = this.detectFullScreen.bind(this);
20 | }
21 |
22 | componentDidMount() {
23 | fscreen.addEventListener("fullscreenchange", this.detectFullScreen);
24 | }
25 |
26 | componentWillUnmount() {
27 | fscreen.removeEventListener("fullscreenchange", this.detectFullScreen);
28 | }
29 |
30 | componentDidUpdate() {
31 | this.handleProps(this.props);
32 | }
33 |
34 | handleProps(props) {
35 | const enabled = fscreen.fullscreenElement === this.node;
36 | if (enabled && !props.enabled) {
37 | this.leaveFullScreen();
38 | } else if (!enabled && props.enabled) {
39 | this.enterFullScreen();
40 | }
41 | }
42 |
43 | detectFullScreen() {
44 | if (this.props.onChange) {
45 | this.props.onChange(fscreen.fullscreenElement === this.node);
46 | }
47 | }
48 |
49 | enterFullScreen() {
50 | if (fscreen.fullscreenEnabled) {
51 | fscreen.requestFullscreen(this.node);
52 | }
53 | }
54 |
55 | leaveFullScreen() {
56 | if (fscreen.fullscreenEnabled) {
57 | fscreen.exitFullscreen();
58 | }
59 | }
60 |
61 | render() {
62 | const className = ["fullscreen"];
63 | if (this.props.enabled) {
64 | className.push("fullscreen-enabled");
65 | }
66 | return (
67 | (this.node = node)}
70 | style={
71 | { height: "100%", width: "100%" }
72 | }
73 | >
74 | {this.props.children}
75 |
76 | );
77 | }
78 | }
79 |
80 | export default FullScreen;
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebook/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
13 | // "public path" at which the app is served.
14 | // webpack needs to know it to put the right