elation-engine
Version:
WebGL/WebVR engine written in Javascript
1,617 lines (1,356 loc) • 246 kB
JavaScript
// Bail in worker threads
if (!(typeof Window == 'undefined' || !(window instanceof Window))) {
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.WebVRPolyfill = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
'use strict';
var has = Object.prototype.hasOwnProperty
, prefix = '~';
/**
* Constructor to create a storage for our `EE` objects.
* An `Events` instance is a plain object whose properties are event names.
*
* @constructor
* @api private
*/
function Events() {}
//
// We try to not inherit from `Object.prototype`. In some engines creating an
// instance in this way is faster than calling `Object.create(null)` directly.
// If `Object.create(null)` is not supported we prefix the event names with a
// character to make sure that the built-in object properties are not
// overridden or used as an attack vector.
//
if (Object.create) {
Events.prototype = Object.create(null);
//
// This hack is needed because the `__proto__` property is still inherited in
// some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5.
//
if (!new Events().__proto__) prefix = false;
}
/**
* Representation of a single event listener.
*
* @param {Function} fn The listener function.
* @param {Mixed} context The context to invoke the listener with.
* @param {Boolean} [once=false] Specify if the listener is a one-time listener.
* @constructor
* @api private
*/
function EE(fn, context, once) {
this.fn = fn;
this.context = context;
this.once = once || false;
}
/**
* Minimal `EventEmitter` interface that is molded against the Node.js
* `EventEmitter` interface.
*
* @constructor
* @api public
*/
function EventEmitter() {
this._events = new Events();
this._eventsCount = 0;
}
/**
* Return an array listing the events for which the emitter has registered
* listeners.
*
* @returns {Array}
* @api public
*/
EventEmitter.prototype.eventNames = function eventNames() {
var names = []
, events
, name;
if (this._eventsCount === 0) return names;
for (name in (events = this._events)) {
if (has.call(events, name)) names.push(prefix ? name.slice(1) : name);
}
if (Object.getOwnPropertySymbols) {
return names.concat(Object.getOwnPropertySymbols(events));
}
return names;
};
/**
* Return the listeners registered for a given event.
*
* @param {String|Symbol} event The event name.
* @param {Boolean} exists Only check if there are listeners.
* @returns {Array|Boolean}
* @api public
*/
EventEmitter.prototype.listeners = function listeners(event, exists) {
var evt = prefix ? prefix + event : event
, available = this._events[evt];
if (exists) return !!available;
if (!available) return [];
if (available.fn) return [available.fn];
for (var i = 0, l = available.length, ee = new Array(l); i < l; i++) {
ee[i] = available[i].fn;
}
return ee;
};
/**
* Calls each of the listeners registered for a given event.
*
* @param {String|Symbol} event The event name.
* @returns {Boolean} `true` if the event had listeners, else `false`.
* @api public
*/
EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) {
var evt = prefix ? prefix + event : event;
if (!this._events[evt]) return false;
var listeners = this._events[evt]
, len = arguments.length
, args
, i;
if (listeners.fn) {
if (listeners.once) this.removeListener(event, listeners.fn, undefined, true);
switch (len) {
case 1: return listeners.fn.call(listeners.context), true;
case 2: return listeners.fn.call(listeners.context, a1), true;
case 3: return listeners.fn.call(listeners.context, a1, a2), true;
case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true;
case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true;
case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true;
}
for (i = 1, args = new Array(len -1); i < len; i++) {
args[i - 1] = arguments[i];
}
listeners.fn.apply(listeners.context, args);
} else {
var length = listeners.length
, j;
for (i = 0; i < length; i++) {
if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true);
switch (len) {
case 1: listeners[i].fn.call(listeners[i].context); break;
case 2: listeners[i].fn.call(listeners[i].context, a1); break;
case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break;
case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break;
default:
if (!args) for (j = 1, args = new Array(len -1); j < len; j++) {
args[j - 1] = arguments[j];
}
listeners[i].fn.apply(listeners[i].context, args);
}
}
}
return true;
};
/**
* Add a listener for a given event.
*
* @param {String|Symbol} event The event name.
* @param {Function} fn The listener function.
* @param {Mixed} [context=this] The context to invoke the listener with.
* @returns {EventEmitter} `this`.
* @api public
*/
EventEmitter.prototype.on = function on(event, fn, context) {
var listener = new EE(fn, context || this)
, evt = prefix ? prefix + event : event;
if (!this._events[evt]) this._events[evt] = listener, this._eventsCount++;
else if (!this._events[evt].fn) this._events[evt].push(listener);
else this._events[evt] = [this._events[evt], listener];
return this;
};
/**
* Add a one-time listener for a given event.
*
* @param {String|Symbol} event The event name.
* @param {Function} fn The listener function.
* @param {Mixed} [context=this] The context to invoke the listener with.
* @returns {EventEmitter} `this`.
* @api public
*/
EventEmitter.prototype.once = function once(event, fn, context) {
var listener = new EE(fn, context || this, true)
, evt = prefix ? prefix + event : event;
if (!this._events[evt]) this._events[evt] = listener, this._eventsCount++;
else if (!this._events[evt].fn) this._events[evt].push(listener);
else this._events[evt] = [this._events[evt], listener];
return this;
};
/**
* Remove the listeners of a given event.
*
* @param {String|Symbol} event The event name.
* @param {Function} fn Only remove the listeners that match this function.
* @param {Mixed} context Only remove the listeners that have this context.
* @param {Boolean} once Only remove one-time listeners.
* @returns {EventEmitter} `this`.
* @api public
*/
EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) {
var evt = prefix ? prefix + event : event;
if (!this._events[evt]) return this;
if (!fn) {
if (--this._eventsCount === 0) this._events = new Events();
else delete this._events[evt];
return this;
}
var listeners = this._events[evt];
if (listeners.fn) {
if (
listeners.fn === fn
&& (!once || listeners.once)
&& (!context || listeners.context === context)
) {
if (--this._eventsCount === 0) this._events = new Events();
else delete this._events[evt];
}
} else {
for (var i = 0, events = [], length = listeners.length; i < length; i++) {
if (
listeners[i].fn !== fn
|| (once && !listeners[i].once)
|| (context && listeners[i].context !== context)
) {
events.push(listeners[i]);
}
}
//
// Reset the array, or remove it completely if we have no more listeners.
//
if (events.length) this._events[evt] = events.length === 1 ? events[0] : events;
else if (--this._eventsCount === 0) this._events = new Events();
else delete this._events[evt];
}
return this;
};
/**
* Remove all listeners, or those of the specified event.
*
* @param {String|Symbol} [event] The event name.
* @returns {EventEmitter} `this`.
* @api public
*/
EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) {
var evt;
if (event) {
evt = prefix ? prefix + event : event;
if (this._events[evt]) {
if (--this._eventsCount === 0) this._events = new Events();
else delete this._events[evt];
}
} else {
this._events = new Events();
this._eventsCount = 0;
}
return this;
};
//
// Alias methods names because people roll like that.
//
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
//
// This function doesn't apply anymore.
//
EventEmitter.prototype.setMaxListeners = function setMaxListeners() {
return this;
};
//
// Expose the prefix.
//
EventEmitter.prefixed = prefix;
//
// Allow `EventEmitter` to be imported as module namespace.
//
EventEmitter.EventEmitter = EventEmitter;
//
// Expose the module.
//
if ('undefined' !== typeof module) {
module.exports = EventEmitter;
}
},{}],2:[function(_dereq_,module,exports){
'use strict';
/* eslint-disable no-unused-vars */
var hasOwnProperty = Object.prototype.hasOwnProperty;
var propIsEnumerable = Object.prototype.propertyIsEnumerable;
function toObject(val) {
if (val === null || val === undefined) {
throw new TypeError('Object.assign cannot be called with null or undefined');
}
return Object(val);
}
function shouldUseNative() {
try {
if (!Object.assign) {
return false;
}
// Detect buggy property enumeration order in older V8 versions.
// https://bugs.chromium.org/p/v8/issues/detail?id=4118
var test1 = new String('abc'); // eslint-disable-line
test1[5] = 'de';
if (Object.getOwnPropertyNames(test1)[0] === '5') {
return false;
}
// https://bugs.chromium.org/p/v8/issues/detail?id=3056
var test2 = {};
for (var i = 0; i < 10; i++) {
test2['_' + String.fromCharCode(i)] = i;
}
var order2 = Object.getOwnPropertyNames(test2).map(function (n) {
return test2[n];
});
if (order2.join('') !== '0123456789') {
return false;
}
// https://bugs.chromium.org/p/v8/issues/detail?id=3056
var test3 = {};
'abcdefghijklmnopqrst'.split('').forEach(function (letter) {
test3[letter] = letter;
});
if (Object.keys(Object.assign({}, test3)).join('') !==
'abcdefghijklmnopqrst') {
return false;
}
return true;
} catch (e) {
// We don't expect any of the above to throw, but better to be safe.
return false;
}
}
module.exports = shouldUseNative() ? Object.assign : function (target, source) {
var from;
var to = toObject(target);
var symbols;
for (var s = 1; s < arguments.length; s++) {
from = Object(arguments[s]);
for (var key in from) {
if (hasOwnProperty.call(from, key)) {
to[key] = from[key];
}
}
if (Object.getOwnPropertySymbols) {
symbols = Object.getOwnPropertySymbols(from);
for (var i = 0; i < symbols.length; i++) {
if (propIsEnumerable.call(from, symbols[i])) {
to[symbols[i]] = from[symbols[i]];
}
}
}
}
return to;
};
},{}],3:[function(_dereq_,module,exports){
/*
* 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 = _dereq_('./util.js');
var WakeLock = _dereq_('./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;
},{"./util.js":22,"./wakelock.js":24}],4:[function(_dereq_,module,exports){
/*
* Copyright 2016 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 CardboardUI = _dereq_('./cardboard-ui.js');
var Util = _dereq_('./util.js');
var WGLUPreserveGLState = _dereq_('./deps/wglu-preserve-state.js');
var distortionVS = [
'attribute vec2 position;',
'attribute vec3 texCoord;',
'varying vec2 vTexCoord;',
'uniform vec4 viewportOffsetScale[2];',
'void main() {',
' vec4 viewport = viewportOffsetScale[int(texCoord.z)];',
' vTexCoord = (texCoord.xy * viewport.zw) + viewport.xy;',
' gl_Position = vec4( position, 1.0, 1.0 );',
'}',
].join('\n');
var distortionFS = [
'precision mediump float;',
'uniform sampler2D diffuse;',
'varying vec2 vTexCoord;',
'void main() {',
' gl_FragColor = texture2D(diffuse, vTexCoord);',
'}',
].join('\n');
/**
* A mesh-based distorter.
*/
function CardboardDistorter(gl) {
this.gl = gl;
this.ctxAttribs = gl.getContextAttributes();
this.meshWidth = 20;
this.meshHeight = 20;
this.bufferScale = WebVRConfig.BUFFER_SCALE;
this.bufferWidth = gl.drawingBufferWidth;
this.bufferHeight = gl.drawingBufferHeight;
// Patching support
this.realBindFramebuffer = gl.bindFramebuffer;
this.realEnable = gl.enable;
this.realDisable = gl.disable;
this.realColorMask = gl.colorMask;
this.realClearColor = gl.clearColor;
this.realViewport = gl.viewport;
if (!Util.isIOS()) {
this.realCanvasWidth = Object.getOwnPropertyDescriptor(gl.canvas.__proto__, 'width');
this.realCanvasHeight = Object.getOwnPropertyDescriptor(gl.canvas.__proto__, 'height');
}
this.isPatched = false;
// State tracking
this.lastBoundFramebuffer = null;
this.cullFace = false;
this.depthTest = false;
this.blend = false;
this.scissorTest = false;
this.stencilTest = false;
this.viewport = [0, 0, 0, 0];
this.colorMask = [true, true, true, true];
this.clearColor = [0, 0, 0, 0];
this.attribs = {
position: 0,
texCoord: 1
};
this.program = Util.linkProgram(gl, distortionVS, distortionFS, this.attribs);
this.uniforms = Util.getProgramUniforms(gl, this.program);
this.viewportOffsetScale = new Float32Array(8);
this.setTextureBounds();
this.vertexBuffer = gl.createBuffer();
this.indexBuffer = gl.createBuffer();
this.indexCount = 0;
this.renderTarget = gl.createTexture();
this.framebuffer = gl.createFramebuffer();
this.depthStencilBuffer = null;
this.depthBuffer = null;
this.stencilBuffer = null;
if (this.ctxAttribs.depth && this.ctxAttribs.stencil) {
this.depthStencilBuffer = gl.createRenderbuffer();
} else if (this.ctxAttribs.depth) {
this.depthBuffer = gl.createRenderbuffer();
} else if (this.ctxAttribs.stencil) {
this.stencilBuffer = gl.createRenderbuffer();
}
this.patch();
this.onResize();
if (!WebVRConfig.CARDBOARD_UI_DISABLED) {
this.cardboardUI = new CardboardUI(gl);
}
};
/**
* Tears down all the resources created by the distorter and removes any
* patches.
*/
CardboardDistorter.prototype.destroy = function() {
var gl = this.gl;
this.unpatch();
gl.deleteProgram(this.program);
gl.deleteBuffer(this.vertexBuffer);
gl.deleteBuffer(this.indexBuffer);
gl.deleteTexture(this.renderTarget);
gl.deleteFramebuffer(this.framebuffer);
if (this.depthStencilBuffer) {
gl.deleteRenderbuffer(this.depthStencilBuffer);
}
if (this.depthBuffer) {
gl.deleteRenderbuffer(this.depthBuffer);
}
if (this.stencilBuffer) {
gl.deleteRenderbuffer(this.stencilBuffer);
}
if (this.cardboardUI) {
this.cardboardUI.destroy();
}
};
/**
* Resizes the backbuffer to match the canvas width and height.
*/
CardboardDistorter.prototype.onResize = function() {
var gl = this.gl;
var self = this;
var glState = [
gl.RENDERBUFFER_BINDING,
gl.TEXTURE_BINDING_2D, gl.TEXTURE0
];
WGLUPreserveGLState(gl, glState, function(gl) {
// Bind real backbuffer and clear it once. We don't need to clear it again
// after that because we're overwriting the same area every frame.
self.realBindFramebuffer.call(gl, gl.FRAMEBUFFER, null);
// Put things in a good state
if (self.scissorTest) { self.realDisable.call(gl, gl.SCISSOR_TEST); }
self.realColorMask.call(gl, true, true, true, true);
self.realViewport.call(gl, 0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
self.realClearColor.call(gl, 0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// Now bind and resize the fake backbuffer
self.realBindFramebuffer.call(gl, gl.FRAMEBUFFER, self.framebuffer);
gl.bindTexture(gl.TEXTURE_2D, self.renderTarget);
gl.texImage2D(gl.TEXTURE_2D, 0, self.ctxAttribs.alpha ? gl.RGBA : gl.RGB,
self.bufferWidth, self.bufferHeight, 0,
self.ctxAttribs.alpha ? gl.RGBA : gl.RGB, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, self.renderTarget, 0);
if (self.ctxAttribs.depth && self.ctxAttribs.stencil) {
gl.bindRenderbuffer(gl.RENDERBUFFER, self.depthStencilBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL,
self.bufferWidth, self.bufferHeight);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT,
gl.RENDERBUFFER, self.depthStencilBuffer);
} else if (self.ctxAttribs.depth) {
gl.bindRenderbuffer(gl.RENDERBUFFER, self.depthBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16,
self.bufferWidth, self.bufferHeight);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT,
gl.RENDERBUFFER, self.depthBuffer);
} else if (self.ctxAttribs.stencil) {
gl.bindRenderbuffer(gl.RENDERBUFFER, self.stencilBuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.STENCIL_INDEX8,
self.bufferWidth, self.bufferHeight);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.STENCIL_ATTACHMENT,
gl.RENDERBUFFER, self.stencilBuffer);
}
if (!gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) {
console.error('Framebuffer incomplete!');
}
self.realBindFramebuffer.call(gl, gl.FRAMEBUFFER, self.lastBoundFramebuffer);
if (self.scissorTest) { self.realEnable.call(gl, gl.SCISSOR_TEST); }
self.realColorMask.apply(gl, self.colorMask);
self.realViewport.apply(gl, self.viewport);
self.realClearColor.apply(gl, self.clearColor);
});
if (this.cardboardUI) {
this.cardboardUI.onResize();
}
};
CardboardDistorter.prototype.patch = function() {
if (this.isPatched) {
return;
}
var self = this;
var canvas = this.gl.canvas;
var gl = this.gl;
if (!Util.isIOS()) {
canvas.width = Util.getScreenWidth() * this.bufferScale;
canvas.height = Util.getScreenHeight() * this.bufferScale;
Object.defineProperty(canvas, 'width', {
configurable: true,
enumerable: true,
get: function() {
return self.bufferWidth;
},
set: function(value) {
self.bufferWidth = value;
self.realCanvasWidth.set.call(canvas, value);
self.onResize();
}
});
Object.defineProperty(canvas, 'height', {
configurable: true,
enumerable: true,
get: function() {
return self.bufferHeight;
},
set: function(value) {
self.bufferHeight = value;
self.realCanvasHeight.set.call(canvas, value);
self.onResize();
}
});
}
this.lastBoundFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING);
if (this.lastBoundFramebuffer == null) {
this.lastBoundFramebuffer = this.framebuffer;
this.gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
}
this.gl.bindFramebuffer = function(target, framebuffer) {
self.lastBoundFramebuffer = framebuffer ? framebuffer : self.framebuffer;
// Silently make calls to bind the default framebuffer bind ours instead.
self.realBindFramebuffer.call(gl, target, self.lastBoundFramebuffer);
};
this.cullFace = gl.getParameter(gl.CULL_FACE);
this.depthTest = gl.getParameter(gl.DEPTH_TEST);
this.blend = gl.getParameter(gl.BLEND);
this.scissorTest = gl.getParameter(gl.SCISSOR_TEST);
this.stencilTest = gl.getParameter(gl.STENCIL_TEST);
gl.enable = function(pname) {
switch (pname) {
case gl.CULL_FACE: self.cullFace = true; break;
case gl.DEPTH_TEST: self.depthTest = true; break;
case gl.BLEND: self.blend = true; break;
case gl.SCISSOR_TEST: self.scissorTest = true; break;
case gl.STENCIL_TEST: self.stencilTest = true; break;
}
self.realEnable.call(gl, pname);
};
gl.disable = function(pname) {
switch (pname) {
case gl.CULL_FACE: self.cullFace = false; break;
case gl.DEPTH_TEST: self.depthTest = false; break;
case gl.BLEND: self.blend = false; break;
case gl.SCISSOR_TEST: self.scissorTest = false; break;
case gl.STENCIL_TEST: self.stencilTest = false; break;
}
self.realDisable.call(gl, pname);
};
this.colorMask = gl.getParameter(gl.COLOR_WRITEMASK);
gl.colorMask = function(r, g, b, a) {
self.colorMask[0] = r;
self.colorMask[1] = g;
self.colorMask[2] = b;
self.colorMask[3] = a;
self.realColorMask.call(gl, r, g, b, a);
};
this.clearColor = gl.getParameter(gl.COLOR_CLEAR_VALUE);
gl.clearColor = function(r, g, b, a) {
self.clearColor[0] = r;
self.clearColor[1] = g;
self.clearColor[2] = b;
self.clearColor[3] = a;
self.realClearColor.call(gl, r, g, b, a);
};
this.viewport = gl.getParameter(gl.VIEWPORT);
gl.viewport = function(x, y, w, h) {
self.viewport[0] = x;
self.viewport[1] = y;
self.viewport[2] = w;
self.viewport[3] = h;
self.realViewport.call(gl, x, y, w, h);
};
this.isPatched = true;
Util.safariCssSizeWorkaround(canvas);
};
CardboardDistorter.prototype.unpatch = function() {
if (!this.isPatched) {
return;
}
var gl = this.gl;
var canvas = this.gl.canvas;
if (!Util.isIOS()) {
Object.defineProperty(canvas, 'width', this.realCanvasWidth);
Object.defineProperty(canvas, 'height', this.realCanvasHeight);
}
canvas.width = this.bufferWidth;
canvas.height = this.bufferHeight;
gl.bindFramebuffer = this.realBindFramebuffer;
gl.enable = this.realEnable;
gl.disable = this.realDisable;
gl.colorMask = this.realColorMask;
gl.clearColor = this.realClearColor;
gl.viewport = this.realViewport;
// Check to see if our fake backbuffer is bound and bind the real backbuffer
// if that's the case.
if (this.lastBoundFramebuffer == this.framebuffer) {
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
this.isPatched = false;
setTimeout(function() {
Util.safariCssSizeWorkaround(canvas);
}, 1);
};
CardboardDistorter.prototype.setTextureBounds = function(leftBounds, rightBounds) {
if (!leftBounds) {
leftBounds = [0, 0, 0.5, 1];
}
if (!rightBounds) {
rightBounds = [0.5, 0, 0.5, 1];
}
// Left eye
this.viewportOffsetScale[0] = leftBounds[0]; // X
this.viewportOffsetScale[1] = leftBounds[1]; // Y
this.viewportOffsetScale[2] = leftBounds[2]; // Width
this.viewportOffsetScale[3] = leftBounds[3]; // Height
// Right eye
this.viewportOffsetScale[4] = rightBounds[0]; // X
this.viewportOffsetScale[5] = rightBounds[1]; // Y
this.viewportOffsetScale[6] = rightBounds[2]; // Width
this.viewportOffsetScale[7] = rightBounds[3]; // Height
};
/**
* Performs distortion pass on the injected backbuffer, rendering it to the real
* backbuffer.
*/
CardboardDistorter.prototype.submitFrame = function() {
var gl = this.gl;
var self = this;
var glState = [];
if (!WebVRConfig.DIRTY_SUBMIT_FRAME_BINDINGS) {
glState.push(
gl.CURRENT_PROGRAM,
gl.ARRAY_BUFFER_BINDING,
gl.ELEMENT_ARRAY_BUFFER_BINDING,
gl.TEXTURE_BINDING_2D, gl.TEXTURE0
);
}
WGLUPreserveGLState(gl, glState, function(gl) {
// Bind the real default framebuffer
self.realBindFramebuffer.call(gl, gl.FRAMEBUFFER, null);
// Make sure the GL state is in a good place
if (self.cullFace) { self.realDisable.call(gl, gl.CULL_FACE); }
if (self.depthTest) { self.realDisable.call(gl, gl.DEPTH_TEST); }
if (self.blend) { self.realDisable.call(gl, gl.BLEND); }
if (self.scissorTest) { self.realDisable.call(gl, gl.SCISSOR_TEST); }
if (self.stencilTest) { self.realDisable.call(gl, gl.STENCIL_TEST); }
self.realColorMask.call(gl, true, true, true, true);
self.realViewport.call(gl, 0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
// If the backbuffer has an alpha channel clear every frame so the page
// doesn't show through.
if (self.ctxAttribs.alpha || Util.isIOS()) {
self.realClearColor.call(gl, 0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
}
// Bind distortion program and mesh
gl.useProgram(self.program);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.indexBuffer);
gl.bindBuffer(gl.ARRAY_BUFFER, self.vertexBuffer);
gl.enableVertexAttribArray(self.attribs.position);
gl.enableVertexAttribArray(self.attribs.texCoord);
gl.vertexAttribPointer(self.attribs.position, 2, gl.FLOAT, false, 20, 0);
gl.vertexAttribPointer(self.attribs.texCoord, 3, gl.FLOAT, false, 20, 8);
gl.activeTexture(gl.TEXTURE0);
gl.uniform1i(self.uniforms.diffuse, 0);
gl.bindTexture(gl.TEXTURE_2D, self.renderTarget);
gl.uniform4fv(self.uniforms.viewportOffsetScale, self.viewportOffsetScale);
// Draws both eyes
gl.drawElements(gl.TRIANGLES, self.indexCount, gl.UNSIGNED_SHORT, 0);
if (self.cardboardUI) {
self.cardboardUI.renderNoState();
}
// Bind the fake default framebuffer again
self.realBindFramebuffer.call(self.gl, gl.FRAMEBUFFER, self.framebuffer);
// If preserveDrawingBuffer == false clear the framebuffer
if (!self.ctxAttribs.preserveDrawingBuffer) {
self.realClearColor.call(gl, 0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
}
if (!WebVRConfig.DIRTY_SUBMIT_FRAME_BINDINGS) {
self.realBindFramebuffer.call(gl, gl.FRAMEBUFFER, self.lastBoundFramebuffer);
}
// Restore state
if (self.cullFace) { self.realEnable.call(gl, gl.CULL_FACE); }
if (self.depthTest) { self.realEnable.call(gl, gl.DEPTH_TEST); }
if (self.blend) { self.realEnable.call(gl, gl.BLEND); }
if (self.scissorTest) { self.realEnable.call(gl, gl.SCISSOR_TEST); }
if (self.stencilTest) { self.realEnable.call(gl, gl.STENCIL_TEST); }
self.realColorMask.apply(gl, self.colorMask);
self.realViewport.apply(gl, self.viewport);
if (self.ctxAttribs.alpha || !self.ctxAttribs.preserveDrawingBuffer) {
self.realClearColor.apply(gl, self.clearColor);
}
});
// Workaround for the fact that Safari doesn't allow us to patch the canvas
// width and height correctly. After each submit frame check to see what the
// real backbuffer size has been set to and resize the fake backbuffer size
// to match.
if (Util.isIOS()) {
var canvas = gl.canvas;
if (canvas.width != self.bufferWidth || canvas.height != self.bufferHeight) {
self.bufferWidth = canvas.width;
self.bufferHeight = canvas.height;
self.onResize();
}
}
};
/**
* Call when the deviceInfo has changed. At this point we need
* to re-calculate the distortion mesh.
*/
CardboardDistorter.prototype.updateDeviceInfo = function(deviceInfo) {
var gl = this.gl;
var self = this;
var glState = [gl.ARRAY_BUFFER_BINDING, gl.ELEMENT_ARRAY_BUFFER_BINDING];
WGLUPreserveGLState(gl, glState, function(gl) {
var vertices = self.computeMeshVertices_(self.meshWidth, self.meshHeight, deviceInfo);
gl.bindBuffer(gl.ARRAY_BUFFER, self.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// Indices don't change based on device parameters, so only compute once.
if (!self.indexCount) {
var indices = self.computeMeshIndices_(self.meshWidth, self.meshHeight);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
self.indexCount = indices.length;
}
});
};
/**
* Build the distortion mesh vertices.
* Based on code from the Unity cardboard plugin.
*/
CardboardDistorter.prototype.computeMeshVertices_ = function(width, height, deviceInfo) {
var vertices = new Float32Array(2 * width * height * 5);
var lensFrustum = deviceInfo.getLeftEyeVisibleTanAngles();
var noLensFrustum = deviceInfo.getLeftEyeNoLensTanAngles();
var viewport = deviceInfo.getLeftEyeVisibleScreenRect(noLensFrustum);
var vidx = 0;
var iidx = 0;
for (var e = 0; e < 2; e++) {
for (var j = 0; j < height; j++) {
for (var i = 0; i < width; i++, vidx++) {
var u = i / (width - 1);
var v = j / (height - 1);
// Grid points regularly spaced in StreoScreen, and barrel distorted in
// the mesh.
var s = u;
var t = v;
var x = Util.lerp(lensFrustum[0], lensFrustum[2], u);
var y = Util.lerp(lensFrustum[3], lensFrustum[1], v);
var d = Math.sqrt(x * x + y * y);
var r = deviceInfo.distortion.distortInverse(d);
var p = x * r / d;
var q = y * r / d;
u = (p - noLensFrustum[0]) / (noLensFrustum[2] - noLensFrustum[0]);
v = (q - noLensFrustum[3]) / (noLensFrustum[1] - noLensFrustum[3]);
// Convert u,v to mesh screen coordinates.
var aspect = deviceInfo.device.widthMeters / deviceInfo.device.heightMeters;
// FIXME: The original Unity plugin multiplied U by the aspect ratio
// and didn't multiply either value by 2, but that seems to get it
// really close to correct looking for me. I hate this kind of "Don't
// know why it works" code though, and wold love a more logical
// explanation of what needs to happen here.
u = (viewport.x + u * viewport.width - 0.5) * 2.0; //* aspect;
v = (viewport.y + v * viewport.height - 0.5) * 2.0;
vertices[(vidx * 5) + 0] = u; // position.x
vertices[(vidx * 5) + 1] = v; // position.y
vertices[(vidx * 5) + 2] = s; // texCoord.x
vertices[(vidx * 5) + 3] = t; // texCoord.y
vertices[(vidx * 5) + 4] = e; // texCoord.z (viewport index)
}
}
var w = lensFrustum[2] - lensFrustum[0];
lensFrustum[0] = -(w + lensFrustum[0]);
lensFrustum[2] = w - lensFrustum[2];
w = noLensFrustum[2] - noLensFrustum[0];
noLensFrustum[0] = -(w + noLensFrustum[0]);
noLensFrustum[2] = w - noLensFrustum[2];
viewport.x = 1 - (viewport.x + viewport.width);
}
return vertices;
}
/**
* Build the distortion mesh indices.
* Based on code from the Unity cardboard plugin.
*/
CardboardDistorter.prototype.computeMeshIndices_ = function(width, height) {
var indices = new Uint16Array(2 * (width - 1) * (height - 1) * 6);
var halfwidth = width / 2;
var halfheight = height / 2;
var vidx = 0;
var iidx = 0;
for (var e = 0; e < 2; e++) {
for (var j = 0; j < height; j++) {
for (var i = 0; i < width; i++, vidx++) {
if (i == 0 || j == 0)
continue;
// Build a quad. Lower right and upper left quadrants have quads with
// the triangle diagonal flipped to get the vignette to interpolate
// correctly.
if ((i <= halfwidth) == (j <= halfheight)) {
// Quad diagonal lower left to upper right.
indices[iidx++] = vidx;
indices[iidx++] = vidx - width - 1;
indices[iidx++] = vidx - width;
indices[iidx++] = vidx - width - 1;
indices[iidx++] = vidx;
indices[iidx++] = vidx - 1;
} else {
// Quad diagonal upper left to lower right.
indices[iidx++] = vidx - 1;
indices[iidx++] = vidx - width;
indices[iidx++] = vidx;
indices[iidx++] = vidx - width;
indices[iidx++] = vidx - 1;
indices[iidx++] = vidx - width - 1;
}
}
}
}
return indices;
};
CardboardDistorter.prototype.getOwnPropertyDescriptor_ = function(proto, attrName) {
var descriptor = Object.getOwnPropertyDescriptor(proto, attrName);
// In some cases (ahem... Safari), the descriptor returns undefined get and
// set fields. In this case, we need to create a synthetic property
// descriptor. This works around some of the issues in
// https://github.com/borismus/webvr-polyfill/issues/46
if (descriptor.get === undefined || descriptor.set === undefined) {
descriptor.configurable = true;
descriptor.enumerable = true;
descriptor.get = function() {
return this.getAttribute(attrName);
};
descriptor.set = function(val) {
this.setAttribute(attrName, val);
};
}
return descriptor;
};
module.exports = CardboardDistorter;
},{"./cardboard-ui.js":5,"./deps/wglu-preserve-state.js":7,"./util.js":22}],5:[function(_dereq_,module,exports){
/*
* Copyright 2016 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 = _dereq_('./util.js');
var WGLUPreserveGLState = _dereq_('./deps/wglu-preserve-state.js');
var uiVS = [
'attribute vec2 position;',
'uniform mat4 projectionMat;',
'void main() {',
' gl_Position = projectionMat * vec4( position, -1.0, 1.0 );',
'}',
].join('\n');
var uiFS = [
'precision mediump float;',
'uniform vec4 color;',
'void main() {',
' gl_FragColor = color;',
'}',
].join('\n');
var DEG2RAD = Math.PI/180.0;
// The gear has 6 identical sections, each spanning 60 degrees.
var kAnglePerGearSection = 60;
// Half-angle of the span of the outer rim.
var kOuterRimEndAngle = 12;
// Angle between the middle of the outer rim and the start of the inner rim.
var kInnerRimBeginAngle = 20;
// Distance from center to outer rim, normalized so that the entire model
// fits in a [-1, 1] x [-1, 1] square.
var kOuterRadius = 1;
// Distance from center to depressed rim, in model units.
var kMiddleRadius = 0.75;
// Radius of the inner hollow circle, in model units.
var kInnerRadius = 0.3125;
// Center line thickness in DP.
var kCenterLineThicknessDp = 4;
// Button width in DP.
var kButtonWidthDp = 28;
// Factor to scale the touch area that responds to the touch.
var kTouchSlopFactor = 1.5;
var Angles = [
0, kOuterRimEndAngle, kInnerRimBeginAngle,
kAnglePerGearSection - kInnerRimBeginAngle,
kAnglePerGearSection - kOuterRimEndAngle
];
/**
* Renders the alignment line and "options" gear. It is assumed that the canvas
* this is rendered into covers the entire screen (or close to it.)
*/
function CardboardUI(gl) {
this.gl = gl;
this.attribs = {
position: 0
};
this.program = Util.linkProgram(gl, uiVS, uiFS, this.attribs);
this.uniforms = Util.getProgramUniforms(gl, this.program);
this.vertexBuffer = gl.createBuffer();
this.gearOffset = 0;
this.gearVertexCount = 0;
this.arrowOffset = 0;
this.arrowVertexCount = 0;
this.projMat = new Float32Array(16);
this.listener = null;
this.onResize();
};
/**
* Tears down all the resources created by the UI renderer.
*/
CardboardUI.prototype.destroy = function() {
var gl = this.gl;
if (this.listener) {
gl.canvas.removeEventListener('click', this.listener, false);
}
gl.deleteProgram(this.program);
gl.deleteBuffer(this.vertexBuffer);
};
/**
* Adds a listene