/* * Copyright 2015 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var Util = require('./util.js'); var WakeLock = require('./wakelock.js'); // Start at a higher number to reduce chance of conflict. var nextDisplayId = 1000; var hasShowDeprecationWarning = false; var defaultLeftBounds = [0, 0, 0.5, 1]; var defaultRightBounds = [0.5, 0, 0.5, 1]; /** * The base class for all VR frame data. */ function VRFrameData() { this.leftProjectionMatrix = new Float32Array(16); this.leftViewMatrix = new Float32Array(16); this.rightProjectionMatrix = new Float32Array(16); this.rightViewMatrix = new Float32Array(16); this.pose = null; }; /** * The base class for all VR displays. */ function VRDisplay() { this.isPolyfilled = true; this.displayId = nextDisplayId++; this.displayName = 'webvr-polyfill displayName'; this.depthNear = 0.01; this.depthFar = 10000.0; this.isConnected = true; this.isPresenting = false; this.capabilities = { hasPosition: false, hasOrientation: false, hasExternalDisplay: false, canPresent: false, maxLayers: 1 }; this.stageParameters = null; // "Private" members. this.waitingForPresent_ = false; this.layer_ = null; this.fullscreenElement_ = null; this.fullscreenWrapper_ = null; this.fullscreenElementCachedStyle_ = null; this.fullscreenEventTarget_ = null; this.fullscreenChangeHandler_ = null; this.fullscreenErrorHandler_ = null; this.wakelock_ = new WakeLock(); } VRDisplay.prototype.getFrameData = function(frameData) { // TODO: Technically this should retain it's value for the duration of a frame // but I doubt that's practical to do in javascript. return Util.frameDataFromPose(frameData, this.getPose(), this); }; VRDisplay.prototype.getPose = function() { // TODO: Technically this should retain it's value for the duration of a frame // but I doubt that's practical to do in javascript. return this.getImmediatePose(); }; VRDisplay.prototype.requestAnimationFrame = function(callback) { return window.requestAnimationFrame(callback); }; VRDisplay.prototype.cancelAnimationFrame = function(id) { return window.cancelAnimationFrame(id); }; VRDisplay.prototype.wrapForFullscreen = function(element) { // Don't wrap in iOS. if (Util.isIOS()) { return element; } if (!this.fullscreenWrapper_) { this.fullscreenWrapper_ = document.createElement('div'); var cssProperties = [ 'height: ' + Math.min(screen.height, screen.width) + 'px !important', 'top: 0 !important', 'left: 0 !important', 'right: 0 !important', 'border: 0', 'margin: 0', 'padding: 0', 'z-index: 999999 !important', 'position: fixed', ]; this.fullscreenWrapper_.setAttribute('style', cssProperties.join('; ') + ';'); this.fullscreenWrapper_.classList.add('webvr-polyfill-fullscreen-wrapper'); } if (this.fullscreenElement_ == element) { return this.fullscreenWrapper_; } // Remove any previously applied wrappers this.removeFullscreenWrapper(); this.fullscreenElement_ = element; var parent = this.fullscreenElement_.parentElement; parent.insertBefore(this.fullscreenWrapper_, this.fullscreenElement_); parent.removeChild(this.fullscreenElement_); this.fullscreenWrapper_.insertBefore(this.fullscreenElement_, this.fullscreenWrapper_.firstChild); this.fullscreenElementCachedStyle_ = this.fullscreenElement_.getAttribute('style'); var self = this; function applyFullscreenElementStyle() { if (!self.fullscreenElement_) { return; } var cssProperties = [ 'position: absolute', 'top: 0', 'left: 0', 'width: ' + Math.max(screen.width, screen.height) + 'px', 'height: ' + Math.min(screen.height, screen.width) + 'px', 'border: 0', 'margin: 0', 'padding: 0', ]; self.fullscreenElement_.setAttribute('style', cssProperties.join('; ') + ';'); } applyFullscreenElementStyle(); return this.fullscreenWrapper_; }; VRDisplay.prototype.removeFullscreenWrapper = function() { if (!this.fullscreenElement_) { return; } var element = this.fullscreenElement_; if (this.fullscreenElementCachedStyle_) { element.setAttribute('style', this.fullscreenElementCachedStyle_); } else { element.removeAttribute('style'); } this.fullscreenElement_ = null; this.fullscreenElementCachedStyle_ = null; var parent = this.fullscreenWrapper_.parentElement; this.fullscreenWrapper_.removeChild(element); parent.insertBefore(element, this.fullscreenWrapper_); parent.removeChild(this.fullscreenWrapper_); return element; }; VRDisplay.prototype.requestPresent = function(layers) { var wasPresenting = this.isPresenting; var self = this; if (!(layers instanceof Array)) { if (!hasShowDeprecationWarning) { console.warn("Using a deprecated form of requestPresent. Should pass in an array of VRLayers."); hasShowDeprecationWarning = true; } layers = [layers]; } return new Promise(function(resolve, reject) { if (!self.capabilities.canPresent) { reject(new Error('VRDisplay is not capable of presenting.')); return; } if (layers.length == 0 || layers.length > self.capabilities.maxLayers) { reject(new Error('Invalid number of layers.')); return; } var incomingLayer = layers[0]; if (!incomingLayer.source) { /* todo: figure out the correct behavior if the source is not provided. see https://github.com/w3c/webvr/issues/58 */ resolve(); return; } var leftBounds = incomingLayer.leftBounds || defaultLeftBounds; var rightBounds = incomingLayer.rightBounds || defaultRightBounds; if (wasPresenting) { // Already presenting, just changing configuration var layer = self.layer_; if (layer.source !== incomingLayer.source) { layer.source = incomingLayer.source; } for (var i = 0; i < 4; i++) { if (layer.leftBounds[i] !== leftBounds[i]) { layer.leftBounds[i] = leftBounds[i]; } if (layer.rightBounds[i] !== rightBounds[i]) { layer.rightBounds[i] = rightBounds[i]; } } resolve(); return; } // Was not already presenting. self.layer_ = { predistorted: incomingLayer.predistorted, source: incomingLayer.source, leftBounds: leftBounds.slice(0), rightBounds: rightBounds.slice(0) }; self.waitingForPresent_ = false; if (self.layer_ && self.layer_.source) { var fullscreenElement = self.wrapForFullscreen(self.layer_.source); function onFullscreenChange() { var actualFullscreenElement = Util.getFullscreenElement(); self.isPresenting = (fullscreenElement === actualFullscreenElement); if (self.isPresenting) { if (screen.orientation && screen.orientation.lock) { screen.orientation.lock('landscape-primary').catch(function(error){ console.error('screen.orientation.lock() failed due to', error.message) }); } self.waitingForPresent_ = false; self.beginPresent_(); resolve(); } else { if (screen.orientation && screen.orientation.unlock) { screen.orientation.unlock(); } self.removeFullscreenWrapper(); self.wakelock_.release(); self.endPresent_(); self.removeFullscreenListeners_(); } self.fireVRDisplayPresentChange_(); } function onFullscreenError() { if (!self.waitingForPresent_) { return; } self.removeFullscreenWrapper(); self.removeFullscreenListeners_(); self.wakelock_.release(); self.waitingForPresent_ = false; self.isPresenting = false; reject(new Error('Unable to present.')); } self.addFullscreenListeners_(fullscreenElement, onFullscreenChange, onFullscreenError); if (Util.requestFullscreen(fullscreenElement)) { self.wakelock_.request(); self.waitingForPresent_ = true; } else if (Util.isIOS()) { // *sigh* Just fake it. self.wakelock_.request(); self.isPresenting = true; self.beginPresent_(); self.fireVRDisplayPresentChange_(); resolve(); } } if (!self.waitingForPresent_ && !Util.isIOS()) { Util.exitFullscreen(); reject(new Error('Unable to present.')); } }); }; VRDisplay.prototype.exitPresent = function() { var wasPresenting = this.isPresenting; var self = this; this.isPresenting = false; this.layer_ = null; this.wakelock_.release(); return new Promise(function(resolve, reject) { if (wasPresenting) { if (!Util.exitFullscreen() && Util.isIOS()) { self.endPresent_(); self.fireVRDisplayPresentChange_(); } resolve(); } else { reject(new Error('Was not presenting to VRDisplay.')); } }); }; VRDisplay.prototype.getLayers = function() { if (this.layer_) { return [this.layer_]; } return []; }; VRDisplay.prototype.fireVRDisplayPresentChange_ = function() { var event = new CustomEvent('vrdisplaypresentchange', {detail: {display: this}}); window.dispatchEvent(event); }; VRDisplay.prototype.addFullscreenListeners_ = function(element, changeHandler, errorHandler) { this.removeFullscreenListeners_(); this.fullscreenEventTarget_ = element; this.fullscreenChangeHandler_ = changeHandler; this.fullscreenErrorHandler_ = errorHandler; if (changeHandler) { if (document.fullscreenEnabled) { element.addEventListener('fullscreenchange', changeHandler, false); } else if (document.webkitFullscreenEnabled) { element.addEventListener('webkitfullscreenchange', changeHandler, false); } else if (document.mozFullScreenEnabled) { document.addEventListener('mozfullscreenchange', changeHandler, false); } else if (document.msFullscreenEnabled) { element.addEventListener('msfullscreenchange', changeHandler, false); } } if (errorHandler) { if (document.fullscreenEnabled) { element.addEventListener('fullscreenerror', errorHandler, false); } else if (document.webkitFullscreenEnabled) { element.addEventListener('webkitfullscreenerror', errorHandler, false); } else if (document.mozFullScreenEnabled) { document.addEventListener('mozfullscreenerror', errorHandler, false); } else if (document.msFullscreenEnabled) { element.addEventListener('msfullscreenerror', errorHandler, false); } } }; VRDisplay.prototype.removeFullscreenListeners_ = function() { if (!this.fullscreenEventTarget_) return; var element = this.fullscreenEventTarget_; if (this.fullscreenChangeHandler_) { var changeHandler = this.fullscreenChangeHandler_; element.removeEventListener('fullscreenchange', changeHandler, false); element.removeEventListener('webkitfullscreenchange', changeHandler, false); document.removeEventListener('mozfullscreenchange', changeHandler, false); element.removeEventListener('msfullscreenchange', changeHandler, false); } if (this.fullscreenErrorHandler_) { var errorHandler = this.fullscreenErrorHandler_; element.removeEventListener('fullscreenerror', errorHandler, false); element.removeEventListener('webkitfullscreenerror', errorHandler, false); document.removeEventListener('mozfullscreenerror', errorHandler, false); element.removeEventListener('msfullscreenerror', errorHandler, false); } this.fullscreenEventTarget_ = null; this.fullscreenChangeHandler_ = null; this.fullscreenErrorHandler_ = null; }; VRDisplay.prototype.beginPresent_ = function() { // Override to add custom behavior when presentation begins. }; VRDisplay.prototype.endPresent_ = function() { // Override to add custom behavior when presentation ends. }; VRDisplay.prototype.submitFrame = function(pose) { // Override to add custom behavior for frame submission. }; VRDisplay.prototype.getEyeParameters = function(whichEye) { // Override to return accurate eye parameters if canPresent is true. return null; }; /* * Deprecated classes */ /** * The base class for all VR devices. (Deprecated) */ function VRDevice() { this.isPolyfilled = true; this.hardwareUnitId = 'webvr-polyfill hardwareUnitId'; this.deviceId = 'webvr-polyfill deviceId'; this.deviceName = 'webvr-polyfill deviceName'; } /** * The base class for all VR HMD devices. (Deprecated) */ function HMDVRDevice() { } HMDVRDevice.prototype = new VRDevice(); /** * The base class for all VR position sensor devices. (Deprecated) */ function PositionSensorVRDevice() { } PositionSensorVRDevice.prototype = new VRDevice(); module.exports.VRFrameData = VRFrameData; module.exports.VRDisplay = VRDisplay; module.exports.VRDevice = VRDevice; module.exports.HMDVRDevice = HMDVRDevice; module.exports.PositionSensorVRDevice = PositionSensorVRDevice;