├── .npmignore ├── examples ├── example.png ├── package.json ├── aframe.html ├── node_modules │ └── three │ │ └── examples │ │ └── js │ │ ├── controls │ │ └── VRControls.js │ │ └── effects │ │ └── VREffect.js ├── basic.html ├── customDom.html └── styling.html ├── .gitignore ├── .eslintrc.json ├── src ├── index.js ├── states.js ├── aframe-component.js ├── webvr-manager.js ├── dom.js └── enter-vr-button.js ├── CONTRIBUTING.md ├── package.json ├── README.md └── LICENSE /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | CONTRIBUTING.md 3 | -------------------------------------------------------------------------------- /examples/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlearchive/webvr-ui/HEAD/examples/example.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # editor files 2 | .idea/ 3 | *.swp 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | 12 | node_modules 13 | 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "google"], 3 | "parserOptions": { 4 | "sourceType": "module" 5 | }, 6 | "env": { 7 | "browser": true, 8 | "es6": true 9 | }, 10 | "rules": { 11 | "max-len": [2,120] 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webvr-ui-example", 3 | "version": "0.0.1", 4 | "description": "Example of using WebVR UI Library", 5 | "authors": [ 6 | "Kyle Phillips ", 7 | "Jonas Jongejan " 8 | ], 9 | "dependencies": { 10 | "three": "^0.84.0", 11 | "webvr-polyfill": "^0.9.26" 12 | }, 13 | "keywords": [ 14 | "vr", 15 | "webvr" 16 | ], 17 | "license": "Apache-2.0", 18 | "browserify": { 19 | "transform": [ 20 | "babelify" 21 | ] 22 | }, 23 | "directories": { 24 | "example": "example" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import WebVRManager from './webvr-manager'; 16 | import State from './states'; 17 | import * as dom from './dom'; 18 | import EnterVRButton from './enter-vr-button'; 19 | import './aframe-component'; 20 | 21 | export { 22 | EnterVRButton, 23 | dom, 24 | State, 25 | WebVRManager, 26 | }; 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to any Google project must be accompanied by a Contributor License 9 | Agreement. This is necessary because you own the copyright to your changes, even 10 | after your contribution becomes part of this project. So this agreement simply 11 | gives us permission to use and redistribute your contributions as part of the 12 | project. Head over to to see your current 13 | agreements on file or to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult [GitHub Help] for more 23 | information on using pull requests. 24 | 25 | [GitHub Help]: https://help.github.com/articles/about-pull-requests/ -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webvr-ui", 3 | "version": "0.10.0", 4 | "description": "Library that creates a UI wrapping the WebVR experience", 5 | "main": "src/index.js", 6 | "authors": [ 7 | "Kyle Phillips ", 8 | "Jonas Jongejan " 9 | ], 10 | "repository" : { 11 | "type" : "git", 12 | "url" : "https://github.com/googlevr/webvr-ui.git" 13 | }, 14 | "scripts": { 15 | "build": "npm run browserify & npm run browserifymin", 16 | "browserify": "browserify src/index.js --standalone webvrui | derequire > build/webvr-ui.js", 17 | "browserifymin": "browserify -g uglifyify src/index.js --standalone webvrui | derequire > build/webvr-ui.min.js", 18 | "start": "budo src/index.js:build/webvr-ui.js --live --verbose --port 3000 -- --standalone webvrui", 19 | "lint": "eslint -c .eslintrc.json src", 20 | "prepublish": "npm run lint && npm run build" 21 | }, 22 | "dependencies": { 23 | "eventemitter3": "^2.0.2", 24 | "screenfull": "^3.0.2" 25 | }, 26 | "devDependencies": { 27 | "babel-preset-es2015": "^6.16.0", 28 | "babelify": "^7.3.0", 29 | "browserify": "^14.0.0", 30 | "budo": "^9.2.1", 31 | "derequire": "^2.0.3", 32 | "eslint": "^3.13.1", 33 | "eslint-config-google": "^0.7.1", 34 | "tape": "^4.6.2", 35 | "uglifyify": "^3.0.4" 36 | }, 37 | "keywords": [ 38 | "vr", 39 | "webvr" 40 | ], 41 | "license": "Apache-2.0", 42 | "browserify": { 43 | "transform": [ 44 | [ 45 | "babelify", 46 | { 47 | "presets": [ 48 | "es2015" 49 | ] 50 | } 51 | ] 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/states.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Not yet presenting, but ready to present 16 | const READY_TO_PRESENT = 'ready'; 17 | 18 | // In presentation mode 19 | const PRESENTING = 'presenting'; 20 | const PRESENTING_FULLSCREEN = 'presenting-fullscreen'; 21 | 22 | // Checking device availability 23 | const PREPARING = 'preparing'; 24 | 25 | // Errors 26 | const ERROR_NO_PRESENTABLE_DISPLAYS = 'error-no-presentable-displays'; 27 | const ERROR_BROWSER_NOT_SUPPORTED = 'error-browser-not-supported'; 28 | const ERROR_REQUEST_TO_PRESENT_REJECTED = 'error-request-to-present-rejected'; 29 | const ERROR_EXIT_PRESENT_REJECTED = 'error-exit-present-rejected'; 30 | const ERROR_REQUEST_STATE_CHANGE_REJECTED = 'error-request-state-change-rejected'; 31 | const ERROR_UNKOWN = 'error-unkown'; 32 | 33 | export default { 34 | READY_TO_PRESENT, 35 | PRESENTING, 36 | PRESENTING_FULLSCREEN, 37 | PREPARING, 38 | ERROR_NO_PRESENTABLE_DISPLAYS, 39 | ERROR_BROWSER_NOT_SUPPORTED, 40 | ERROR_REQUEST_TO_PRESENT_REJECTED, 41 | ERROR_EXIT_PRESENT_REJECTED, 42 | ERROR_REQUEST_STATE_CHANGE_REJECTED, 43 | ERROR_UNKOWN, 44 | }; 45 | -------------------------------------------------------------------------------- /examples/aframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WebVR UI | A-Frame Example 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/aframe-component.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* global AFRAME */ 16 | 17 | import EnterVRButton from './enter-vr-button'; 18 | import State from './states'; 19 | 20 | if (typeof AFRAME !== 'undefined' && AFRAME) { 21 | AFRAME.registerComponent('webvr-ui', { 22 | dependencies: ['canvas'], 23 | 24 | schema: { 25 | enabled: {type: 'boolean', default: true}, 26 | color: {type: 'string', default: 'white'}, 27 | background: {type: 'string', default: 'black'}, 28 | corners: {type: 'string', default: 'square'}, 29 | disabledOpacity: {type: 'number', default: 0.5}, 30 | 31 | textEnterVRTitle: {type: 'string'}, 32 | textExitVRTitle: {type: 'string'}, 33 | textVRNotFoundTitle: {type: 'string'}, 34 | }, 35 | 36 | init: function() { 37 | }, 38 | 39 | update: function() { 40 | let scene = document.querySelector('a-scene'); 41 | scene.setAttribute('vr-mode-ui', {enabled: !this.data.enabled}); 42 | 43 | if (this.data.enabled) { 44 | if (this.enterVREl) { 45 | return; 46 | } 47 | 48 | let options = { 49 | color: this.data.color, 50 | background: this.data.background, 51 | corners: this.data.corners, 52 | disabledOpacity: this.data.disabledOpacity, 53 | textEnterVRTitle: this.data.textEnterVRTitle, 54 | textExitVRTitle: this.data.textExitVRTitle, 55 | textVRNotFoundTitle: this.data.textVRNotFoundTitle, 56 | onRequestStateChange: function(state) { 57 | if (state == State.PRESENTING) { 58 | scene.enterVR(); 59 | } else { 60 | scene.exitVR(); 61 | } 62 | return false; 63 | }, 64 | }; 65 | 66 | let enterVR = this.enterVR = new EnterVRButton(scene.canvas, options); 67 | 68 | this.enterVREl = enterVR.domElement; 69 | 70 | document.body.appendChild(enterVR.domElement); 71 | 72 | enterVR.domElement.style.position = 'absolute'; 73 | enterVR.domElement.style.bottom = '10px'; 74 | enterVR.domElement.style.left = '50%'; 75 | enterVR.domElement.style.transform = 'translate(-50%, -50%)'; 76 | enterVR.domElement.style.textAlign = 'center'; 77 | } else { 78 | if (this.enterVREl) { 79 | this.enterVREl.parentNode.removeChild(this.enterVREl); 80 | this.enterVR.remove(); 81 | } 82 | } 83 | }, 84 | 85 | remove: function() { 86 | if (this.enterVREl) { 87 | this.enterVREl.parentNode.removeChild(this.enterVREl); 88 | this.enterVR.remove(); 89 | } 90 | }, 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /examples/node_modules/three/examples/js/controls/VRControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author dmarcos / https://github.com/dmarcos 3 | * @author mrdoob / http://mrdoob.com 4 | */ 5 | 6 | THREE.VRControls = function ( object, onError ) { 7 | 8 | var scope = this; 9 | 10 | var vrDisplay, vrDisplays; 11 | 12 | var standingMatrix = new THREE.Matrix4(); 13 | 14 | var frameData = null; 15 | 16 | if ( 'VRFrameData' in window ) { 17 | 18 | frameData = new VRFrameData(); 19 | 20 | } 21 | 22 | function gotVRDisplays( displays ) { 23 | 24 | vrDisplays = displays; 25 | 26 | if ( displays.length > 0 ) { 27 | 28 | vrDisplay = displays[ 0 ]; 29 | 30 | } else { 31 | 32 | if ( onError ) onError( 'VR input not available.' ); 33 | 34 | } 35 | 36 | } 37 | 38 | if ( navigator.getVRDisplays ) { 39 | 40 | navigator.getVRDisplays().then( gotVRDisplays ).catch ( function () { 41 | 42 | console.warn( 'THREE.VRControls: Unable to get VR Displays' ); 43 | 44 | } ); 45 | 46 | } 47 | 48 | // the Rift SDK returns the position in meters 49 | // this scale factor allows the user to define how meters 50 | // are converted to scene units. 51 | 52 | this.scale = 1; 53 | 54 | // If true will use "standing space" coordinate system where y=0 is the 55 | // floor and x=0, z=0 is the center of the room. 56 | this.standing = false; 57 | 58 | // Distance from the users eyes to the floor in meters. Used when 59 | // standing=true but the VRDisplay doesn't provide stageParameters. 60 | this.userHeight = 1.6; 61 | 62 | this.getVRDisplay = function () { 63 | 64 | return vrDisplay; 65 | 66 | }; 67 | 68 | this.setVRDisplay = function ( value ) { 69 | 70 | vrDisplay = value; 71 | 72 | }; 73 | 74 | this.getVRDisplays = function () { 75 | 76 | console.warn( 'THREE.VRControls: getVRDisplays() is being deprecated.' ); 77 | return vrDisplays; 78 | 79 | }; 80 | 81 | this.getStandingMatrix = function () { 82 | 83 | return standingMatrix; 84 | 85 | }; 86 | 87 | this.update = function () { 88 | 89 | if ( vrDisplay ) { 90 | 91 | var pose; 92 | 93 | if ( vrDisplay.getFrameData ) { 94 | 95 | vrDisplay.getFrameData( frameData ); 96 | pose = frameData.pose; 97 | 98 | } else if ( vrDisplay.getPose ) { 99 | 100 | pose = vrDisplay.getPose(); 101 | 102 | } 103 | 104 | if ( pose.orientation !== null ) { 105 | 106 | object.quaternion.fromArray( pose.orientation ); 107 | 108 | } 109 | 110 | if ( pose.position !== null ) { 111 | 112 | object.position.fromArray( pose.position ); 113 | 114 | } else { 115 | 116 | object.position.set( 0, 0, 0 ); 117 | 118 | } 119 | 120 | if ( this.standing ) { 121 | 122 | if ( vrDisplay.stageParameters ) { 123 | 124 | object.updateMatrix(); 125 | 126 | standingMatrix.fromArray( vrDisplay.stageParameters.sittingToStandingTransform ); 127 | object.applyMatrix( standingMatrix ); 128 | 129 | } else { 130 | 131 | object.position.setY( object.position.y + this.userHeight ); 132 | 133 | } 134 | 135 | } 136 | 137 | object.position.multiplyScalar( scope.scale ); 138 | 139 | } 140 | 141 | }; 142 | 143 | this.resetPose = function () { 144 | 145 | if ( vrDisplay ) { 146 | 147 | vrDisplay.resetPose(); 148 | 149 | } 150 | 151 | }; 152 | 153 | this.resetSensor = function () { 154 | 155 | console.warn( 'THREE.VRControls: .resetSensor() is now .resetPose().' ); 156 | this.resetPose(); 157 | 158 | }; 159 | 160 | this.zeroSensor = function () { 161 | 162 | console.warn( 'THREE.VRControls: .zeroSensor() is now .resetPose().' ); 163 | this.resetPose(); 164 | 165 | }; 166 | 167 | this.dispose = function () { 168 | 169 | vrDisplay = null; 170 | 171 | }; 172 | 173 | }; 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebVR UI 2 | 3 | 4 | A javascript library allowing easily to create the Enter VR button a [WebVR](https://webvr.info) site. It will automatically detect the support in the browser and show correct messages to the user. The intention for the library is to create an easy way to make a button solving as many of the common use cases of WebVR as possible, and show some best practices for how to work with WebVR. 5 | 6 | The library also supports adding a *Enter Fullscreen* link that allows entering a mode where on desktop you can use the mouse to drag around, and on mobile rotate the camera based on the gyroscope without rendering in stereoscopic mode (also known as *Magic Window*) 7 | 8 | ### Examples 9 | - [Basic usage](http://googlevr.github.io/webvr-ui/examples/basic.html) Shows how to simply add a button with the default styling to a site using three.js ([source](/examples/basic.html)) 10 | - [A-Frame usage](http://googlevr.github.io/webvr-ui/examples/aframe.html) Shows how to use the library with [A-Frame](https://aframe.io) ([source](/examples/aframe.html)) 11 | - [Styling options](http://googlevr.github.io/webvr-ui/examples/styling.html) Shows how customize the styling through options ([source](/examples/styling.html)) 12 | - [Custom DOM](http://googlevr.github.io/webvr-ui/examples/customDom.html) Shows how to use the library with a custom DOM for the button [A-Frame](https://aframe.io) ([source](/examples/customDom.html)) 13 | 14 | 15 | ## Library Usage 16 | ### Include WebVR UI 17 | Get the library either by cloning, downloading or installing through npm `npm install webvr-ui` 18 | 19 | Include the ES5 transpiled library in a script tag 20 | 21 | ```html 22 | 23 | ``` 24 | 25 | or include it in your ES2015 code 26 | 27 | ```javascript 28 | import * as webvrui from 'webvr-ui'; 29 | ``` 30 | 31 | The constructor for the button needs the dom element of the WebGL canvas. To use it together with the `THREE.WebGLRenderer`, do something like this 32 | 33 | ```javascript 34 | var renderer = new THREE.WebGLRenderer(); 35 | 36 | var options = {} 37 | var enterVR = new webvrui.EnterVRButton(renderer.domElement, options); 38 | document.body.appendChild(enterVR.domElement); 39 | ``` 40 | 41 | ### A-Frame 42 | To use the button in [A-Frame](https://aframe.io/), include the library as above, and add `webvr-ui` to the scene. 43 | 44 | ```html 45 | 46 | ... 47 | 48 | ``` 49 | 50 | This will disable the default UI and add a *Enter VR* button to the document DOM. All the styling and text options bellow are also available. 51 | 52 | 53 | ### Options 54 | These are the supported options in `EnterVRButton`. All options are optional. 55 | 56 | **Styling** 57 | 58 | - `color`: Set the text and icon color *(default: `rgb(80,168,252)`)* 59 | - `background`: Set the background color, set to `false` for no background *(default: `false`)* 60 | - `corners`: Choose the corner radius. Can either be `'square'` or `'round'` or a number representing pixel radius *(default: `'square'`)* 61 | - `disabledOpacity`: The opacity of the button when disabled *(default: `0.5`)* 62 | - `domElement`: Provide a DOM element to use instead of default build in DOM. See [Custom DOM example](http://googlevr.github.io/webvr-ui/examples/customDom.html) for more details how to use. 63 | - `injectCSS`: Set to false to disable CSS injection of button style *(default: `true`)* 64 | 65 | **Text** 66 | 67 | - `textEnterVRTitle`: The text in the button prompting to begin presenting *(default: `'ENTER VR'`)* 68 | - `textExitVRTitle`: The text in the button prompting to begin presenting *(default: `'EXIT VR'`)* 69 | - `textVRNotFoundTitle`: The text in the button when there is no VR headset found *(default: `'VR NOT FOUND'`)* 70 | 71 | **Function Hooks** 72 | 73 | - `beforeEnter():Promise`: Function called right before entering VR. Must return a Promise. Gives the opportunity to provide custom messaging or other changes before the experience is presented. 74 | - `beforeExit():Promise`: Function called right before exiting VR. Must return a promise. Gives the opportunity to update UI or other changes before the presentation is exited. 75 | - `onRequestStateChange(state):boolean`: A function called before state is changed, use to intercept entering or exiting VR for example. Return `true` to continue with default behavior, or `false` to stop the default behavior. 76 | 77 | 78 | ### Events 79 | The following events will be broadcasted by `EnterVRButton`, and can be subscribed to using the function `.on([event])` on the button. 80 | - `ready` Event called when VR support is first detected, the `VRDisplay` is provided as the first parameter. 81 | - `enter` Event called when user enters VR, the `VRDisplay` is provided as the first parameter. 82 | - `exit` Event called when user exits VR, the `VRDisplay` is provided as the first parameter. 83 | - `error` Event called when an error occurs, i.e. VR is not supported, an `Error` is provided as the first parameter. 84 | - `hide` Event called when button is hidden 85 | - `show` Event called when button is shown 86 | 87 | 88 | ### Functions 89 | These are some of the functions that can be called on the EnterVRButton 90 | 91 | - `setTitle(title)` Change the text in the button. 92 | - `setTooltip(tooltip)` Change the hover tooltip of the button. 93 | - `show()` / `hide()` Change the visibility of the button. 94 | - `disable()` / `enable()` Change the disabled state of the button. 95 | - `getVRDisplay():Promise` Returns a Promise returning the VRDisplay associated to the button. 96 | - `isPresenting():boolean`: Returns `true` if the canvas associated to the button is presenting in fullscreen or VR mode. 97 | - `requestEnterVR():Promise`: Requests to start presenting. Must be called from a user action ([read more](https://w3c.github.io/webvr/#dom-vrdisplay-requestpresent)) 98 | - `requestEnterFullscreen():Promise`: Requests to enter fullscreen mode if its supported in the browser. 99 | - `requestExit():Promise`: Request exiting presentation mode. 100 | 101 | ## Development 102 | To run the example, install dependencies 103 | 104 | ``` 105 | npm install 106 | ``` 107 | 108 | and start the watcher and server (available on [localhost:3000/examples/basic.html](http://localhost:3000/examples/basic.html)) 109 | 110 | ``` 111 | npm start 112 | ``` 113 | 114 | To build the transpiled es5 version of the library, run 115 | 116 | ``` 117 | npm run build 118 | ``` 119 | 120 | and the library will be build to `build/webvr-ui.js` 121 | -------------------------------------------------------------------------------- /src/webvr-manager.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import State from './states'; 16 | import EventEmitter from 'eventemitter3'; 17 | import screenfull from 'screenfull'; 18 | 19 | /** 20 | * WebVR Manager is a utility to handle VR displays 21 | */ 22 | export default class WebVRManager extends EventEmitter { 23 | 24 | /** 25 | * Construct a new WebVRManager 26 | */ 27 | constructor() { 28 | super(); 29 | this.state = State.PREPARING; 30 | 31 | // Bind vr display present change event to __onVRDisplayPresentChange 32 | this.__onVRDisplayPresentChange = this.__onVRDisplayPresentChange.bind(this); 33 | window.addEventListener('vrdisplaypresentchange', this.__onVRDisplayPresentChange); 34 | 35 | this.__onChangeFullscreen = this.__onChangeFullscreen.bind(this); 36 | if (screenfull.enabled) { 37 | document.addEventListener(screenfull.raw.fullscreenchange, this.__onChangeFullscreen); 38 | } 39 | } 40 | 41 | /** 42 | * Check if the browser is compatible with WebVR and has headsets. 43 | * @return {Promise} 44 | */ 45 | checkDisplays() { 46 | return WebVRManager.getVRDisplay() 47 | .then((display) => { 48 | this.defaultDisplay = display; 49 | this.__setState(State.READY_TO_PRESENT); 50 | return display; 51 | }) 52 | .catch((e) => { 53 | delete this.defaultDisplay; 54 | if (e.name == 'NO_DISPLAYS') { 55 | this.__setState(State.ERROR_NO_PRESENTABLE_DISPLAYS); 56 | } else if (e.name == 'WEBVR_UNSUPPORTED') { 57 | this.__setState(State.ERROR_BROWSER_NOT_SUPPORTED); 58 | } else { 59 | this.__setState(State.ERROR_UNKOWN); 60 | } 61 | }); 62 | } 63 | 64 | /** 65 | * clean up object for garbage collection 66 | */ 67 | remove() { 68 | window.removeEventListener('vrdisplaypresentchange', this.__onVRDisplayPresentChange); 69 | if (screenfull.enabled) { 70 | document.removeEventListener(screenfull.raw.fullscreenchanged, this.__onChangeFullscreen); 71 | } 72 | 73 | this.removeAllListeners(); 74 | } 75 | 76 | /** 77 | * returns promise returning list of available VR displays. 78 | * @return {Promise} 79 | */ 80 | static getVRDisplay() { 81 | return new Promise((resolve, reject) => { 82 | if (!navigator || !navigator.getVRDisplays) { 83 | let e = new Error('Browser not supporting WebVR'); 84 | e.name = 'WEBVR_UNSUPPORTED'; 85 | reject(e); 86 | return; 87 | } 88 | 89 | const rejectNoDisplay = ()=> { 90 | // No displays are found. 91 | let e = new Error('No displays found'); 92 | e.name = 'NO_DISPLAYS'; 93 | reject(e); 94 | }; 95 | 96 | navigator.getVRDisplays().then( 97 | function(displays) { 98 | // Promise succeeds, but check if there are any displays actually. 99 | for (let i = 0; i < displays.length; i++) { 100 | if (displays[i].capabilities.canPresent) { 101 | resolve(displays[i]); 102 | break; 103 | } 104 | } 105 | 106 | rejectNoDisplay(); 107 | }, 108 | rejectNoDisplay); 109 | }); 110 | } 111 | 112 | /** 113 | * Enter presentation mode with your set VR display 114 | * @param {VRDisplay} display the display to request present on 115 | * @param {HTMLCanvasElement} canvas 116 | * @return {Promise.} 117 | */ 118 | enterVR(display, canvas) { 119 | this.presentedSource = canvas; 120 | return display.requestPresent([{ 121 | source: canvas, 122 | }]) 123 | .then( 124 | ()=> {}, 125 | // this could fail if: 126 | // 1. Display `canPresent` is false 127 | // 2. Canvas is invalid 128 | // 3. not executed via user interaction 129 | ()=> this.__setState(State.ERROR_REQUEST_TO_PRESENT_REJECTED) 130 | ); 131 | } 132 | 133 | /** 134 | * Exit presentation mode on display 135 | * @param {VRDisplay} display 136 | * @return {Promise.} 137 | */ 138 | exitVR(display) { 139 | return display.exitPresent() 140 | .then( 141 | ()=> { 142 | this.presentedSource = undefined; 143 | }, 144 | // this could fail if: 145 | // 1. exit requested while not currently presenting 146 | ()=> this.__setState(State.ERROR_EXIT_PRESENT_REJECTED) 147 | ); 148 | } 149 | 150 | /** 151 | * Enter fullscreen mode 152 | * @param {HTMLCanvasElement} canvas 153 | * @return {boolean} 154 | */ 155 | enterFullscreen(canvas) { 156 | if (screenfull.enabled) { 157 | screenfull.request(canvas); 158 | } else { 159 | // iOS 160 | this.__setState(State.PRESENTING_FULLSCREEN); 161 | } 162 | return true; 163 | } 164 | 165 | /** 166 | * Exit fullscreen mode 167 | * @return {boolean} 168 | */ 169 | exitFullscreen() { 170 | if (screenfull.enabled && screenfull.isFullscreen) { 171 | screenfull.exit(); 172 | } else if (this.state == State.PRESENTING_FULLSCREEN) { 173 | this.checkDisplays(); 174 | } 175 | return true; 176 | } 177 | 178 | /** 179 | * Change the state of the manager 180 | * @param {State} state 181 | * @private 182 | */ 183 | __setState(state) { 184 | if (state != this.state) { 185 | this.emit('change', state, this.state); 186 | this.state = state; 187 | } 188 | } 189 | 190 | /** 191 | * Triggered on fullscreen change event 192 | * @param {Event} e 193 | * @private 194 | */ 195 | __onChangeFullscreen(e) { 196 | if (screenfull.isFullscreen) { 197 | if(this.state != State.PRESENTING) { 198 | this.__setState(State.PRESENTING_FULLSCREEN); 199 | } 200 | } else { 201 | this.checkDisplays(); 202 | } 203 | } 204 | 205 | /** 206 | * Triggered on vr present change 207 | * @param {Event} event 208 | * @private 209 | */ 210 | __onVRDisplayPresentChange(event) { 211 | try { 212 | let display; 213 | if(event.display) { 214 | // In chrome its supplied on the event 215 | display = event.display; 216 | } else if(event.detail && event.detail.display) { 217 | // Polyfill stores display under detail 218 | display = event.detail.display; 219 | } 220 | 221 | if(display && display.isPresenting && display.getLayers()[0].source !== this.presentedSource) { 222 | // this means a different instance of WebVRManager has requested to present 223 | return; 224 | } 225 | 226 | const isPresenting = this.defaultDisplay && this.defaultDisplay.isPresenting; 227 | this.__setState(isPresenting ? State.PRESENTING : State.READY_TO_PRESENT); 228 | } catch(err) { 229 | // continue regardless of error 230 | } 231 | } 232 | 233 | } 234 | -------------------------------------------------------------------------------- /src/dom.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const _LOGO_SCALE = 0.8; 16 | let _WEBVR_UI_CSS_INJECTED = {}; 17 | 18 | /** 19 | * Generate the innerHTML for the button 20 | * 21 | * @return {string} html of the button as string 22 | * @param {string} cssPrefix 23 | * @param {Number} height 24 | * @private 25 | */ 26 | const generateInnerHTML = (cssPrefix, height)=> { 27 | const logoHeight = height*_LOGO_SCALE; 28 | const svgString = generateVRIconString(cssPrefix, logoHeight) + generateNoVRIconString(cssPrefix, logoHeight); 29 | 30 | return ``; 34 | }; 35 | 36 | /** 37 | * Inject the CSS string to the head of the document 38 | * 39 | * @param {string} cssText the css to inject 40 | */ 41 | export const injectCSS = (cssText)=> { 42 | // Create the css 43 | const style = document.createElement('style'); 44 | style.innerHTML = cssText; 45 | 46 | let head = document.getElementsByTagName('head')[0]; 47 | head.insertBefore(style, head.firstChild); 48 | }; 49 | 50 | /** 51 | * Generate DOM element view for button 52 | * 53 | * @return {HTMLElement} 54 | * @param {Object} options 55 | */ 56 | export const createDefaultView = (options)=> { 57 | const fontSize = options.height / 3; 58 | if (options.injectCSS) { 59 | // Check that css isnt already injected 60 | if (!_WEBVR_UI_CSS_INJECTED[options.cssprefix]) { 61 | injectCSS(generateCSS(options, fontSize)); 62 | _WEBVR_UI_CSS_INJECTED[options.cssprefix] = true; 63 | } 64 | } 65 | 66 | const el = document.createElement('div'); 67 | el.innerHTML = generateInnerHTML(options.cssprefix, fontSize); 68 | return el.firstChild; 69 | }; 70 | 71 | 72 | export const createVRIcon = (cssPrefix, height)=>{ 73 | const el = document.createElement('div'); 74 | el.innerHTML = generateVRIconString(cssPrefix, height); 75 | return el.firstChild; 76 | }; 77 | 78 | export const createNoVRIcon = (cssPrefix, height)=>{ 79 | const el = document.createElement('div'); 80 | el.innerHTML = generateNoVRIconString(cssPrefix, height); 81 | return el.firstChild; 82 | }; 83 | 84 | 85 | const generateVRIconString = (cssPrefix, height)=> { 86 | let aspect = 28 / 18; 87 | return ` 89 | 96 | `; 97 | }; 98 | 99 | const generateNoVRIconString = (cssPrefix, height)=>{ 100 | let aspect = 28 / 18; 101 | return ` 103 | 107 | 110 | 112 | `; 113 | }; 114 | 115 | /** 116 | * Generate the CSS string to inject 117 | * 118 | * @param {Object} options 119 | * @param {Number} [fontSize=18] 120 | * @return {string} 121 | */ 122 | export const generateCSS = (options, fontSize=18)=> { 123 | const height = options.height; 124 | const borderWidth = 2; 125 | const borderColor = options.background ? options.background : options.color; 126 | const cssPrefix = options.cssprefix; 127 | 128 | let borderRadius; 129 | if (options.corners == 'round') { 130 | borderRadius = options.height / 2; 131 | } else if (options.corners == 'square') { 132 | borderRadius = 2; 133 | } else { 134 | borderRadius = options.corners; 135 | } 136 | 137 | return (` 138 | @font-face { 139 | font-family: 'Karla'; 140 | font-style: normal; 141 | font-weight: 400; 142 | src: local('Karla'), local('Karla-Regular'), 143 | url(https://fonts.gstatic.com/s/karla/v5/31P4mP32i98D9CEnGyeX9Q.woff2) format('woff2'); 144 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; 145 | } 146 | @font-face { 147 | font-family: 'Karla'; 148 | font-style: normal; 149 | font-weight: 400; 150 | src: local('Karla'), local('Karla-Regular'), 151 | url(https://fonts.gstatic.com/s/karla/v5/Zi_e6rBgGqv33BWF8WTq8g.woff2) format('woff2'); 152 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, 153 | U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; 154 | } 155 | 156 | button.${cssPrefix}-button { 157 | font-family: 'Karla', sans-serif; 158 | 159 | border: ${borderColor} ${borderWidth}px solid; 160 | border-radius: ${borderRadius}px; 161 | box-sizing: border-box; 162 | background: ${options.background ? options.background : 'none'}; 163 | 164 | height: ${height}px; 165 | min-width: ${fontSize * 9.6}px; 166 | display: inline-block; 167 | position: relative; 168 | 169 | cursor: pointer; 170 | } 171 | 172 | button.${cssPrefix}-button:focus { 173 | outline: none; 174 | } 175 | 176 | /* 177 | * Logo 178 | */ 179 | 180 | .${cssPrefix}-logo { 181 | width: ${height}px; 182 | height: ${height}px; 183 | position: absolute; 184 | top:0px; 185 | left:0px; 186 | width: ${height - 4}px; 187 | height: ${height - 4}px; 188 | } 189 | .${cssPrefix}-svg { 190 | fill: ${options.color}; 191 | margin-top: ${(height - fontSize * _LOGO_SCALE) / 2 - 2}px; 192 | margin-left: ${height / 3 }px; 193 | } 194 | .${cssPrefix}-svg-error { 195 | fill: ${options.color}; 196 | display:none; 197 | margin-top: ${(height - 28 / 18 * fontSize * _LOGO_SCALE) / 2 - 2}px; 198 | margin-left: ${height / 3 }px; 199 | } 200 | 201 | 202 | /* 203 | * Title 204 | */ 205 | 206 | .${cssPrefix}-title { 207 | color: ${options.color}; 208 | position: relative; 209 | font-size: ${fontSize}px; 210 | padding-left: ${height * 1.05}px; 211 | padding-right: ${(borderRadius - 10 < 5) ? height / 3 : borderRadius - 10}px; 212 | } 213 | 214 | /* 215 | * disabled 216 | */ 217 | 218 | button.${cssPrefix}-button[disabled=true] { 219 | opacity: ${options.disabledOpacity}; 220 | cursor: default; 221 | } 222 | 223 | button.${cssPrefix}-button[disabled=true] > .${cssPrefix}-logo > .${cssPrefix}-svg { 224 | display:none; 225 | } 226 | 227 | button.${cssPrefix}-button[disabled=true] > .${cssPrefix}-logo > .${cssPrefix}-svg-error { 228 | display:initial; 229 | } 230 | `); 231 | }; 232 | -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebVR UI | Basic Example 6 | 7 | 8 | 9 | 10 | 11 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
90 |

WebVR UI Basic Example

91 |

Example showing how to enter VR and without headset

92 | 93 |
94 | 95 |
96 | Try it without a headset 97 | 98 | or get set up in VR. 99 | 100 | 101 |
102 |
103 | 104 | 105 | 106 |
107 | 108 | 109 | 110 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /examples/customDom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebVR UI | Custom DOM Example 6 | 7 | 8 | 9 | 10 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 |

WebVR UI Custom DOM Example

72 |

Example showing how to use the library with custom dom elements

73 | 74 | 75 |
76 | 77 | 78 |
79 | 80 | 81 | 82 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /examples/styling.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebVR UI | Styling Example 6 | 7 | 8 | 9 | 10 | 11 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
125 |
126 | 127 |
128 |

WebVR UI Styling Example

129 |

Example showing how to the Enter VR button can be styled in different ways through options

130 |
131 | 132 | 133 |
134 | 135 | 136 | 265 | 266 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /src/enter-vr-button.js: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import WebVRManager from './webvr-manager'; 16 | import {createDefaultView} from './dom'; 17 | import State from './states'; 18 | import EventEmitter from 'eventemitter3'; 19 | 20 | /** 21 | * A button to allow easy-entry and messaging around a WebVR experience 22 | * @class 23 | */ 24 | export default class EnterVRButton extends EventEmitter { 25 | /** 26 | * Construct a new Enter VR Button 27 | * @constructor 28 | * @param {HTMLCanvasElement} sourceCanvas the canvas that you want to present in WebVR 29 | * @param {Object} [options] optional parameters 30 | * @param {HTMLElement} [options.domElement] provide your own domElement to bind to 31 | * @param {Boolean} [options.injectCSS=true] set to false if you want to write your own styles 32 | * @param {Function} [options.beforeEnter] should return a promise, opportunity to intercept request to enter 33 | * @param {Function} [options.beforeExit] should return a promise, opportunity to intercept request to exit 34 | * @param {Function} [options.onRequestStateChange] set to a function returning false to prevent default state changes 35 | * @param {string} [options.textEnterVRTitle] set the text for Enter VR 36 | * @param {string} [options.textVRNotFoundTitle] set the text for when a VR display is not found 37 | * @param {string} [options.textExitVRTitle] set the text for exiting VR 38 | * @param {string} [options.color] text and icon color 39 | * @param {string} [options.background] set to false for no brackground or a color 40 | * @param {string} [options.corners] set to 'round', 'square' or pixel value representing the corner radius 41 | * @param {string} [options.disabledOpacity] set opacity of button dom when disabled 42 | * @param {string} [options.cssprefix] set to change the css prefix from default 'webvr-ui' 43 | */ 44 | constructor(sourceCanvas, options) { 45 | super(); 46 | options = options || {}; 47 | 48 | options.color = options.color || 'rgb(80,168,252)'; 49 | options.background = options.background || false; 50 | options.disabledOpacity = options.disabledOpacity === undefined ? 0.5 : options.disabledOpacity; 51 | options.height = options.height || 55; 52 | options.corners = options.corners || 'square'; 53 | options.cssprefix = options.cssprefix || 'webvr-ui'; 54 | 55 | options.textEnterVRTitle = options.textEnterVRTitle || 'ENTER VR'; 56 | options.textVRNotFoundTitle = options.textVRNotFoundTitle || 'VR NOT FOUND'; 57 | options.textExitVRTitle = options.textExitVRTitle || 'EXIT VR'; 58 | 59 | options.onRequestStateChange = options.onRequestStateChange || (() => true); 60 | // Currently `beforeEnter` is unsupported by Firefox 61 | options.beforeEnter = options.beforeEnter || undefined; 62 | options.beforeExit = options.beforeExit || (()=> new Promise((resolve)=> resolve())); 63 | 64 | options.injectCSS = options.injectCSS !== false; 65 | 66 | this.options = options; 67 | 68 | 69 | this.sourceCanvas = sourceCanvas; 70 | 71 | // Pass in your own domElement if you really dont want to use ours 72 | this.domElement = options.domElement || createDefaultView(options); 73 | this.__defaultDisplayStyle = this.domElement.style.display || 'initial'; 74 | 75 | // Create WebVR Manager 76 | this.manager = new WebVRManager(); 77 | this.manager.checkDisplays(); 78 | this.manager.addListener('change', (state)=> this.__onStateChange(state)); 79 | 80 | // Bind button click events to __onClick 81 | this.domElement.addEventListener('click', ()=> this.__onEnterVRClick()); 82 | 83 | this.__forceDisabled = false; 84 | this.setTitle(this.options.textEnterVRTitle); 85 | } 86 | 87 | /** 88 | * Set the title of the button 89 | * @param {string} text 90 | * @return {EnterVRButton} 91 | */ 92 | setTitle(text) { 93 | this.domElement.title = text; 94 | ifChild(this.domElement, this.options.cssprefix, 'title', (title)=> { 95 | if (!text) { 96 | title.style.display = 'none'; 97 | } else { 98 | title.innerText = text; 99 | title.style.display = 'initial'; 100 | } 101 | }); 102 | 103 | return this; 104 | } 105 | 106 | /** 107 | * Set the tooltip of the button 108 | * @param {string} tooltip 109 | * @return {EnterVRButton} 110 | */ 111 | setTooltip(tooltip) { 112 | this.domElement.title = tooltip; 113 | return this; 114 | } 115 | 116 | /** 117 | * Show the button 118 | * @return {EnterVRButton} 119 | */ 120 | show() { 121 | this.domElement.style.display = this.__defaultDisplayStyle; 122 | this.emit('show'); 123 | return this; 124 | } 125 | 126 | /** 127 | * Hide the button 128 | * @return {EnterVRButton} 129 | */ 130 | hide() { 131 | this.domElement.style.display = 'none'; 132 | this.emit('hide'); 133 | return this; 134 | } 135 | 136 | /** 137 | * Enable the button 138 | * @return {EnterVRButton} 139 | */ 140 | enable() { 141 | this.__setDisabledAttribute(false); 142 | this.__forceDisabled = false; 143 | return this; 144 | } 145 | 146 | /** 147 | * Disable the button from being clicked 148 | * @return {EnterVRButton} 149 | */ 150 | disable() { 151 | this.__setDisabledAttribute(true); 152 | this.__forceDisabled = true; 153 | return this; 154 | } 155 | 156 | /** 157 | * clean up object for garbage collection 158 | */ 159 | remove() { 160 | this.manager.remove(); 161 | 162 | if (this.domElement.parentElement) { 163 | this.domElement.parentElement.removeChild(this.domElement); 164 | } 165 | } 166 | 167 | /** 168 | * Returns a promise getting the VRDisplay used 169 | * @return {Promise.} 170 | */ 171 | getVRDisplay() { 172 | return WebVRManager.getVRDisplay(); 173 | } 174 | 175 | /** 176 | * Check if the canvas the button is connected to is currently presenting 177 | * @return {boolean} 178 | */ 179 | isPresenting() { 180 | return this.state === State.PRESENTING || this.state == State.PRESENTING_FULLSCREEN; 181 | } 182 | 183 | /** 184 | * Request entering VR 185 | * @return {Promise} 186 | */ 187 | requestEnterVR() { 188 | return new Promise((resolve, reject)=> { 189 | if (this.options.onRequestStateChange(State.PRESENTING)) { 190 | if(this.options.beforeEnter) { 191 | return this.options.beforeEnter() 192 | .then(()=> this.manager.enterVR(this.manager.defaultDisplay, this.sourceCanvas)) 193 | .then(resolve); 194 | } else { 195 | return this.manager.enterVR(this.manager.defaultDisplay, this.sourceCanvas) 196 | .then(resolve); 197 | } 198 | } else { 199 | reject(new Error(State.ERROR_REQUEST_STATE_CHANGE_REJECTED)); 200 | } 201 | }); 202 | } 203 | 204 | /** 205 | * Request exiting presentation mode 206 | * @return {Promise} 207 | */ 208 | requestExit() { 209 | const initialState = this.state; 210 | 211 | return new Promise((resolve, reject)=> { 212 | if (this.options.onRequestStateChange(State.READY_TO_PRESENT)) { 213 | return this.options.beforeExit() 214 | .then(()=> 215 | // if we were presenting VR, exit VR, if we are 216 | // exiting fullscreen, exit fullscreen 217 | initialState === State.PRESENTING ? 218 | this.manager.exitVR(this.manager.defaultDisplay) : 219 | this.manager.exitFullscreen()) 220 | .then(resolve); 221 | } else { 222 | reject(new Error(State.ERROR_REQUEST_STATE_CHANGE_REJECTED)); 223 | } 224 | }); 225 | } 226 | 227 | /** 228 | * Request entering the site in fullscreen, but not VR 229 | * @return {Promise} 230 | */ 231 | requestEnterFullscreen() { 232 | return new Promise((resolve, reject)=> { 233 | if (this.options.onRequestStateChange(State.PRESENTING_FULLSCREEN)) { 234 | if(this.options.beforeEnter) { 235 | return this.options.beforeEnter() 236 | .then(()=>this.manager.enterFullscreen(this.sourceCanvas)) 237 | .then(resolve); 238 | } else { 239 | return this.manager.enterFullscreen(this.sourceCanvas) 240 | .then(resolve); 241 | } 242 | } else { 243 | reject(new Error(State.ERROR_REQUEST_STATE_CHANGE_REJECTED)); 244 | } 245 | }); 246 | } 247 | 248 | /** 249 | * Set the disabled attribute 250 | * @param {boolean} disabled 251 | * @private 252 | */ 253 | __setDisabledAttribute(disabled) { 254 | if (disabled || this.__forceDisabled) { 255 | this.domElement.setAttribute('disabled', 'true'); 256 | } else { 257 | this.domElement.removeAttribute('disabled'); 258 | } 259 | } 260 | 261 | /** 262 | * Handling click event from button 263 | * @private 264 | */ 265 | __onEnterVRClick() { 266 | if (this.state == State.READY_TO_PRESENT) { 267 | this.requestEnterVR(); 268 | } else if (this.isPresenting()) { 269 | this.requestExit(); 270 | } 271 | } 272 | 273 | /** 274 | * @param {State} state the state that its transitioning to 275 | * @private 276 | */ 277 | __onStateChange(state) { 278 | if (state != this.state) { 279 | if (this.state === State.PRESENTING || this.state === State.PRESENTING_FULLSCREEN) { 280 | this.emit('exit', this.manager.defaultDisplay); 281 | } 282 | this.state = state; 283 | 284 | switch (state) { 285 | case State.READY_TO_PRESENT: 286 | this.show(); 287 | this.setTitle(this.options.textEnterVRTitle); 288 | if (this.manager.defaultDisplay) { 289 | this.setTooltip('Enter VR using ' + this.manager.defaultDisplay.displayName); 290 | } 291 | this.__setDisabledAttribute(false); 292 | this.emit('ready', this.manager.defaultDisplay); 293 | break; 294 | 295 | case State.PRESENTING: 296 | case State.PRESENTING_FULLSCREEN: 297 | if (!this.manager.defaultDisplay || 298 | !this.manager.defaultDisplay.capabilities.hasExternalDisplay || 299 | state == State.PRESENTING_FULLSCREEN) { 300 | this.hide(); 301 | } 302 | this.setTitle(this.options.textExitVRTitle); 303 | this.__setDisabledAttribute(false); 304 | this.emit('enter', this.manager.defaultDisplay); 305 | break; 306 | 307 | // Error states 308 | case State.ERROR_BROWSER_NOT_SUPPORTED: 309 | this.show(); 310 | this.setTitle(this.options.textVRNotFoundTitle); 311 | this.setTooltip('Browser not supported'); 312 | this.__setDisabledAttribute(true); 313 | this.emit('error', new Error(state)); 314 | break; 315 | 316 | case State.ERROR_NO_PRESENTABLE_DISPLAYS: 317 | this.show(); 318 | this.setTitle(this.options.textVRNotFoundTitle); 319 | this.setTooltip('No VR headset found.'); 320 | this.__setDisabledAttribute(true); 321 | this.emit('error', new Error(state)); 322 | break; 323 | 324 | case State.ERROR_REQUEST_TO_PRESENT_REJECTED: 325 | this.show(); 326 | this.setTitle(this.options.textVRNotFoundTitle); 327 | this.setTooltip('Something went wrong trying to start presenting to your headset.'); 328 | this.__setDisabledAttribute(true); 329 | this.emit('error', new Error(state)); 330 | break; 331 | 332 | case State.ERROR_EXIT_PRESENT_REJECTED: 333 | default: 334 | this.show(); 335 | this.setTitle(this.options.textVRNotFoundTitle); 336 | this.setTooltip('Unknown error.'); 337 | this.__setDisabledAttribute(true); 338 | this.emit('error', new Error(state)); 339 | } 340 | } 341 | } 342 | } 343 | 344 | /** 345 | * Function checking if a specific css class exists as child of element. 346 | * 347 | * @param {HTMLElement} el element to find child in 348 | * @param {string} cssPrefix css prefix of button 349 | * @param {string} suffix class name 350 | * @param {function} fn function to call if child is found 351 | * @private 352 | */ 353 | const ifChild = (el, cssPrefix, suffix, fn)=> { 354 | const c = el.querySelector('.' + cssPrefix + '-' + suffix); 355 | c && fn(c); 356 | }; 357 | -------------------------------------------------------------------------------- /examples/node_modules/three/examples/js/effects/VREffect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author dmarcos / https://github.com/dmarcos 3 | * @author mrdoob / http://mrdoob.com 4 | * 5 | * WebVR Spec: http://mozvr.github.io/webvr-spec/webvr.html 6 | * 7 | * Firefox: http://mozvr.com/downloads/ 8 | * Chromium: https://webvr.info/get-chrome 9 | * 10 | */ 11 | 12 | THREE.VREffect = function( renderer, onError ) { 13 | 14 | var vrDisplay, vrDisplays; 15 | var eyeTranslationL = new THREE.Vector3(); 16 | var eyeTranslationR = new THREE.Vector3(); 17 | var renderRectL, renderRectR; 18 | 19 | var frameData = null; 20 | 21 | if ( 'VRFrameData' in window ) { 22 | 23 | frameData = new window.VRFrameData(); 24 | 25 | } 26 | 27 | function gotVRDisplays( displays ) { 28 | 29 | vrDisplays = displays; 30 | 31 | if ( displays.length > 0 ) { 32 | 33 | vrDisplay = displays[ 0 ]; 34 | 35 | } else { 36 | 37 | if ( onError ) onError( 'HMD not available' ); 38 | 39 | } 40 | 41 | } 42 | 43 | if ( navigator.getVRDisplays ) { 44 | 45 | navigator.getVRDisplays().then( gotVRDisplays ).catch( function() { 46 | 47 | console.warn( 'THREE.VREffect: Unable to get VR Displays' ); 48 | 49 | } ); 50 | 51 | } 52 | 53 | // 54 | 55 | this.isPresenting = false; 56 | this.scale = 1; 57 | 58 | var scope = this; 59 | 60 | var rendererSize = renderer.getSize(); 61 | var rendererUpdateStyle = false; 62 | var rendererPixelRatio = renderer.getPixelRatio(); 63 | 64 | this.getVRDisplay = function() { 65 | 66 | return vrDisplay; 67 | 68 | }; 69 | 70 | this.setVRDisplay = function( value ) { 71 | 72 | vrDisplay = value; 73 | 74 | }; 75 | 76 | this.getVRDisplays = function() { 77 | 78 | console.warn( 'THREE.VREffect: getVRDisplays() is being deprecated.' ); 79 | return vrDisplays; 80 | 81 | }; 82 | 83 | this.setSize = function( width, height, updateStyle ) { 84 | 85 | rendererSize = { width: width, height: height }; 86 | rendererUpdateStyle = updateStyle; 87 | 88 | if ( scope.isPresenting ) { 89 | 90 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); 91 | renderer.setPixelRatio( 1 ); 92 | renderer.setSize( eyeParamsL.renderWidth * 2, eyeParamsL.renderHeight, false ); 93 | 94 | } else { 95 | 96 | renderer.setPixelRatio( rendererPixelRatio ); 97 | renderer.setSize( width, height, updateStyle ); 98 | 99 | } 100 | 101 | }; 102 | 103 | // VR presentation 104 | 105 | var canvas = renderer.domElement; 106 | var defaultLeftBounds = [ 0.0, 0.0, 0.5, 1.0 ]; 107 | var defaultRightBounds = [ 0.5, 0.0, 0.5, 1.0 ]; 108 | 109 | function onVRDisplayPresentChange() { 110 | 111 | var wasPresenting = scope.isPresenting; 112 | scope.isPresenting = vrDisplay !== undefined && vrDisplay.isPresenting; 113 | 114 | if ( scope.isPresenting ) { 115 | 116 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); 117 | var eyeWidth = eyeParamsL.renderWidth; 118 | var eyeHeight = eyeParamsL.renderHeight; 119 | 120 | if ( ! wasPresenting ) { 121 | 122 | rendererPixelRatio = renderer.getPixelRatio(); 123 | rendererSize = renderer.getSize(); 124 | 125 | renderer.setPixelRatio( 1 ); 126 | renderer.setSize( eyeWidth * 2, eyeHeight, false ); 127 | 128 | } 129 | 130 | } else if ( wasPresenting ) { 131 | 132 | renderer.setPixelRatio( rendererPixelRatio ); 133 | renderer.setSize( rendererSize.width, rendererSize.height, rendererUpdateStyle ); 134 | 135 | } 136 | 137 | } 138 | 139 | window.addEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); 140 | 141 | this.setFullScreen = function( boolean ) { 142 | 143 | return new Promise( function( resolve, reject ) { 144 | 145 | if ( vrDisplay === undefined ) { 146 | 147 | reject( new Error( 'No VR hardware found.' ) ); 148 | return; 149 | 150 | } 151 | 152 | if ( scope.isPresenting === boolean ) { 153 | 154 | resolve(); 155 | return; 156 | 157 | } 158 | 159 | if ( boolean ) { 160 | 161 | resolve( vrDisplay.requestPresent( [ { source: canvas } ] ) ); 162 | 163 | } else { 164 | 165 | resolve( vrDisplay.exitPresent() ); 166 | 167 | } 168 | 169 | } ); 170 | 171 | }; 172 | 173 | this.requestPresent = function() { 174 | 175 | return this.setFullScreen( true ); 176 | 177 | }; 178 | 179 | this.exitPresent = function() { 180 | 181 | return this.setFullScreen( false ); 182 | 183 | }; 184 | 185 | this.requestAnimationFrame = function( f ) { 186 | 187 | if ( vrDisplay !== undefined ) { 188 | 189 | return vrDisplay.requestAnimationFrame( f ); 190 | 191 | } else { 192 | 193 | return window.requestAnimationFrame( f ); 194 | 195 | } 196 | 197 | }; 198 | 199 | this.cancelAnimationFrame = function( h ) { 200 | 201 | if ( vrDisplay !== undefined ) { 202 | 203 | vrDisplay.cancelAnimationFrame( h ); 204 | 205 | } else { 206 | 207 | window.cancelAnimationFrame( h ); 208 | 209 | } 210 | 211 | }; 212 | 213 | this.submitFrame = function() { 214 | 215 | if ( vrDisplay !== undefined && scope.isPresenting ) { 216 | 217 | vrDisplay.submitFrame(); 218 | 219 | } 220 | 221 | }; 222 | 223 | this.autoSubmitFrame = true; 224 | 225 | // render 226 | 227 | var cameraL = new THREE.PerspectiveCamera(); 228 | cameraL.layers.enable( 1 ); 229 | 230 | var cameraR = new THREE.PerspectiveCamera(); 231 | cameraR.layers.enable( 2 ); 232 | 233 | this.render = function( scene, camera, renderTarget, forceClear ) { 234 | 235 | if ( vrDisplay && scope.isPresenting ) { 236 | 237 | var autoUpdate = scene.autoUpdate; 238 | 239 | if ( autoUpdate ) { 240 | 241 | scene.updateMatrixWorld(); 242 | scene.autoUpdate = false; 243 | 244 | } 245 | 246 | var eyeParamsL = vrDisplay.getEyeParameters( 'left' ); 247 | var eyeParamsR = vrDisplay.getEyeParameters( 'right' ); 248 | 249 | eyeTranslationL.fromArray( eyeParamsL.offset ); 250 | eyeTranslationR.fromArray( eyeParamsR.offset ); 251 | 252 | if ( Array.isArray( scene ) ) { 253 | 254 | console.warn( 'THREE.VREffect.render() no longer supports arrays. Use object.layers instead.' ); 255 | scene = scene[ 0 ]; 256 | 257 | } 258 | 259 | // When rendering we don't care what the recommended size is, only what the actual size 260 | // of the backbuffer is. 261 | var size = renderer.getSize(); 262 | var layers = vrDisplay.getLayers(); 263 | var leftBounds; 264 | var rightBounds; 265 | 266 | if ( layers.length ) { 267 | 268 | var layer = layers[ 0 ]; 269 | 270 | leftBounds = layer.leftBounds !== null && layer.leftBounds.length === 4 ? layer.leftBounds : defaultLeftBounds; 271 | rightBounds = layer.rightBounds !== null && layer.rightBounds.length === 4 ? layer.rightBounds : defaultRightBounds; 272 | 273 | } else { 274 | 275 | leftBounds = defaultLeftBounds; 276 | rightBounds = defaultRightBounds; 277 | 278 | } 279 | 280 | renderRectL = { 281 | x: Math.round( size.width * leftBounds[ 0 ] ), 282 | y: Math.round( size.height * leftBounds[ 1 ] ), 283 | width: Math.round( size.width * leftBounds[ 2 ] ), 284 | height: Math.round( size.height * leftBounds[ 3 ] ) 285 | }; 286 | renderRectR = { 287 | x: Math.round( size.width * rightBounds[ 0 ] ), 288 | y: Math.round( size.height * rightBounds[ 1 ] ), 289 | width: Math.round( size.width * rightBounds[ 2 ] ), 290 | height: Math.round( size.height * rightBounds[ 3 ] ) 291 | }; 292 | 293 | if ( renderTarget ) { 294 | 295 | renderer.setRenderTarget( renderTarget ); 296 | renderTarget.scissorTest = true; 297 | 298 | } else { 299 | 300 | renderer.setRenderTarget( null ); 301 | renderer.setScissorTest( true ); 302 | 303 | } 304 | 305 | if ( renderer.autoClear || forceClear ) renderer.clear(); 306 | 307 | if ( camera.parent === null ) camera.updateMatrixWorld(); 308 | 309 | camera.matrixWorld.decompose( cameraL.position, cameraL.quaternion, cameraL.scale ); 310 | camera.matrixWorld.decompose( cameraR.position, cameraR.quaternion, cameraR.scale ); 311 | 312 | var scale = this.scale; 313 | cameraL.translateOnAxis( eyeTranslationL, scale ); 314 | cameraR.translateOnAxis( eyeTranslationR, scale ); 315 | 316 | if ( vrDisplay.getFrameData ) { 317 | 318 | vrDisplay.depthNear = camera.near; 319 | vrDisplay.depthFar = camera.far; 320 | 321 | vrDisplay.getFrameData( frameData ); 322 | 323 | cameraL.projectionMatrix.elements = frameData.leftProjectionMatrix; 324 | cameraR.projectionMatrix.elements = frameData.rightProjectionMatrix; 325 | 326 | } else { 327 | 328 | cameraL.projectionMatrix = fovToProjection( eyeParamsL.fieldOfView, true, camera.near, camera.far ); 329 | cameraR.projectionMatrix = fovToProjection( eyeParamsR.fieldOfView, true, camera.near, camera.far ); 330 | 331 | } 332 | 333 | // render left eye 334 | if ( renderTarget ) { 335 | 336 | renderTarget.viewport.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 337 | renderTarget.scissor.set( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 338 | 339 | } else { 340 | 341 | renderer.setViewport( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 342 | renderer.setScissor( renderRectL.x, renderRectL.y, renderRectL.width, renderRectL.height ); 343 | 344 | } 345 | renderer.render( scene, cameraL, renderTarget, forceClear ); 346 | 347 | // render right eye 348 | if ( renderTarget ) { 349 | 350 | renderTarget.viewport.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 351 | renderTarget.scissor.set( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 352 | 353 | } else { 354 | 355 | renderer.setViewport( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 356 | renderer.setScissor( renderRectR.x, renderRectR.y, renderRectR.width, renderRectR.height ); 357 | 358 | } 359 | renderer.render( scene, cameraR, renderTarget, forceClear ); 360 | 361 | if ( renderTarget ) { 362 | 363 | renderTarget.viewport.set( 0, 0, size.width, size.height ); 364 | renderTarget.scissor.set( 0, 0, size.width, size.height ); 365 | renderTarget.scissorTest = false; 366 | renderer.setRenderTarget( null ); 367 | 368 | } else { 369 | 370 | renderer.setViewport( 0, 0, size.width, size.height ); 371 | renderer.setScissorTest( false ); 372 | 373 | } 374 | 375 | if ( autoUpdate ) { 376 | 377 | scene.autoUpdate = true; 378 | 379 | } 380 | 381 | if ( scope.autoSubmitFrame ) { 382 | 383 | scope.submitFrame(); 384 | 385 | } 386 | 387 | return; 388 | 389 | } 390 | 391 | // Regular render mode if not HMD 392 | 393 | renderer.render( scene, camera, renderTarget, forceClear ); 394 | 395 | }; 396 | 397 | this.dispose = function() { 398 | 399 | window.removeEventListener( 'vrdisplaypresentchange', onVRDisplayPresentChange, false ); 400 | 401 | }; 402 | 403 | // 404 | 405 | function fovToNDCScaleOffset( fov ) { 406 | 407 | var pxscale = 2.0 / ( fov.leftTan + fov.rightTan ); 408 | var pxoffset = ( fov.leftTan - fov.rightTan ) * pxscale * 0.5; 409 | var pyscale = 2.0 / ( fov.upTan + fov.downTan ); 410 | var pyoffset = ( fov.upTan - fov.downTan ) * pyscale * 0.5; 411 | return { scale: [ pxscale, pyscale ], offset: [ pxoffset, pyoffset ] }; 412 | 413 | } 414 | 415 | function fovPortToProjection( fov, rightHanded, zNear, zFar ) { 416 | 417 | rightHanded = rightHanded === undefined ? true : rightHanded; 418 | zNear = zNear === undefined ? 0.01 : zNear; 419 | zFar = zFar === undefined ? 10000.0 : zFar; 420 | 421 | var handednessScale = rightHanded ? - 1.0 : 1.0; 422 | 423 | // start with an identity matrix 424 | var mobj = new THREE.Matrix4(); 425 | var m = mobj.elements; 426 | 427 | // and with scale/offset info for normalized device coords 428 | var scaleAndOffset = fovToNDCScaleOffset( fov ); 429 | 430 | // X result, map clip edges to [-w,+w] 431 | m[ 0 * 4 + 0 ] = scaleAndOffset.scale[ 0 ]; 432 | m[ 0 * 4 + 1 ] = 0.0; 433 | m[ 0 * 4 + 2 ] = scaleAndOffset.offset[ 0 ] * handednessScale; 434 | m[ 0 * 4 + 3 ] = 0.0; 435 | 436 | // Y result, map clip edges to [-w,+w] 437 | // Y offset is negated because this proj matrix transforms from world coords with Y=up, 438 | // but the NDC scaling has Y=down (thanks D3D?) 439 | m[ 1 * 4 + 0 ] = 0.0; 440 | m[ 1 * 4 + 1 ] = scaleAndOffset.scale[ 1 ]; 441 | m[ 1 * 4 + 2 ] = - scaleAndOffset.offset[ 1 ] * handednessScale; 442 | m[ 1 * 4 + 3 ] = 0.0; 443 | 444 | // Z result (up to the app) 445 | m[ 2 * 4 + 0 ] = 0.0; 446 | m[ 2 * 4 + 1 ] = 0.0; 447 | m[ 2 * 4 + 2 ] = zFar / ( zNear - zFar ) * - handednessScale; 448 | m[ 2 * 4 + 3 ] = ( zFar * zNear ) / ( zNear - zFar ); 449 | 450 | // W result (= Z in) 451 | m[ 3 * 4 + 0 ] = 0.0; 452 | m[ 3 * 4 + 1 ] = 0.0; 453 | m[ 3 * 4 + 2 ] = handednessScale; 454 | m[ 3 * 4 + 3 ] = 0.0; 455 | 456 | mobj.transpose(); 457 | 458 | return mobj; 459 | 460 | } 461 | 462 | function fovToProjection( fov, rightHanded, zNear, zFar ) { 463 | 464 | var DEG2RAD = Math.PI / 180.0; 465 | 466 | var fovPort = { 467 | upTan: Math.tan( fov.upDegrees * DEG2RAD ), 468 | downTan: Math.tan( fov.downDegrees * DEG2RAD ), 469 | leftTan: Math.tan( fov.leftDegrees * DEG2RAD ), 470 | rightTan: Math.tan( fov.rightDegrees * DEG2RAD ) 471 | }; 472 | 473 | return fovPortToProjection( fovPort, rightHanded, zNear, zFar ); 474 | 475 | } 476 | 477 | }; 478 | --------------------------------------------------------------------------------