mirror of
https://github.com/servo/servo.git
synced 2025-06-18 22:34:30 +01:00
5939 lines
229 KiB
JavaScript
5939 lines
229 KiB
JavaScript
(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';
|
|
/* 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;
|
|
};
|
|
|
|
},{}],2:[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 changed = false;
|
|
var layer = self.layer_;
|
|
if (layer.source !== incomingLayer.source) {
|
|
layer.source = incomingLayer.source;
|
|
changed = true;
|
|
}
|
|
|
|
for (var i = 0; i < 4; i++) {
|
|
if (layer.leftBounds[i] !== leftBounds[i]) {
|
|
layer.leftBounds[i] = leftBounds[i];
|
|
changed = true;
|
|
}
|
|
if (layer.rightBounds[i] !== rightBounds[i]) {
|
|
layer.rightBounds[i] = rightBounds[i];
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
self.fireVRDisplayPresentChange_();
|
|
}
|
|
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: {vrdisplay: this}});
|
|
window.dispatchEvent(event);
|
|
};
|
|
|
|
VRDisplay.prototype.addFullscreenListeners_ = function(element, changeHandler, errorHandler) {
|
|
this.removeFullscreenListeners_();
|
|
|
|
this.fullscreenEventTarget_ = element;
|
|
this.fullscreenChangeHandler_ = changeHandler;
|
|
this.fullscreenErrorHandler_ = errorHandler;
|
|
|
|
if (changeHandler) {
|
|
element.addEventListener('fullscreenchange', changeHandler, false);
|
|
element.addEventListener('webkitfullscreenchange', changeHandler, false);
|
|
document.addEventListener('mozfullscreenchange', changeHandler, false);
|
|
element.addEventListener('msfullscreenchange', changeHandler, false);
|
|
}
|
|
|
|
if (errorHandler) {
|
|
element.addEventListener('fullscreenerror', errorHandler, false);
|
|
element.addEventListener('webkitfullscreenerror', errorHandler, false);
|
|
document.addEventListener('mozfullscreenerror', errorHandler, false);
|
|
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}],3:[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.onResize();
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(canvas, 'height', {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: function() {
|
|
return self.bufferHeight;
|
|
},
|
|
set: function(value) {
|
|
self.bufferHeight = 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":4,"./deps/wglu-preserve-state.js":6,"./util.js":22}],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 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 listener to clicks on the gear and back icons
|
|
*/
|
|
CardboardUI.prototype.listen = function(optionsCallback, backCallback) {
|
|
var canvas = this.gl.canvas;
|
|
this.listener = function(event) {
|
|
var midline = canvas.clientWidth / 2;
|
|
var buttonSize = kButtonWidthDp * kTouchSlopFactor;
|
|
// Check to see if the user clicked on (or around) the gear icon
|
|
if (event.clientX > midline - buttonSize &&
|
|
event.clientX < midline + buttonSize &&
|
|
event.clientY > canvas.clientHeight - buttonSize) {
|
|
optionsCallback(event);
|
|
}
|
|
// Check to see if the user clicked on (or around) the back icon
|
|
else if (event.clientX < buttonSize && event.clientY < buttonSize) {
|
|
backCallback(event);
|
|
}
|
|
};
|
|
canvas.addEventListener('click', this.listener, false);
|
|
};
|
|
|
|
/**
|
|
* Builds the UI mesh.
|
|
*/
|
|
CardboardUI.prototype.onResize = function() {
|
|
var gl = this.gl;
|
|
var self = this;
|
|
|
|
var glState = [
|
|
gl.ARRAY_BUFFER_BINDING
|
|
];
|
|
|
|
WGLUPreserveGLState(gl, glState, function(gl) {
|
|
var vertices = [];
|
|
|
|
var midline = gl.drawingBufferWidth / 2;
|
|
|
|
// Assumes your canvas width and height is scaled proportionately.
|
|
// TODO(smus): The following causes buttons to become huge on iOS, but seems
|
|
// like the right thing to do. For now, added a hack. But really, investigate why.
|
|
var dps = (gl.drawingBufferWidth / (screen.width * window.devicePixelRatio));
|
|
if (!Util.isIOS()) {
|
|
dps *= window.devicePixelRatio;
|
|
}
|
|
|
|
var lineWidth = kCenterLineThicknessDp * dps / 2;
|
|
var buttonSize = kButtonWidthDp * kTouchSlopFactor * dps;
|
|
var buttonScale = kButtonWidthDp * dps / 2;
|
|
var buttonBorder = ((kButtonWidthDp * kTouchSlopFactor) - kButtonWidthDp) * dps;
|
|
|
|
// Build centerline
|
|
vertices.push(midline - lineWidth, buttonSize);
|
|
vertices.push(midline - lineWidth, gl.drawingBufferHeight);
|
|
vertices.push(midline + lineWidth, buttonSize);
|
|
vertices.push(midline + lineWidth, gl.drawingBufferHeight);
|
|
|
|
// Build gear
|
|
self.gearOffset = (vertices.length / 2);
|
|
|
|
function addGearSegment(theta, r) {
|
|
var angle = (90 - theta) * DEG2RAD;
|
|
var x = Math.cos(angle);
|
|
var y = Math.sin(angle);
|
|
vertices.push(kInnerRadius * x * buttonScale + midline, kInnerRadius * y * buttonScale + buttonScale);
|
|
vertices.push(r * x * buttonScale + midline, r * y * buttonScale + buttonScale);
|
|
}
|
|
|
|
for (var i = 0; i <= 6; i++) {
|
|
var segmentTheta = i * kAnglePerGearSection;
|
|
|
|
addGearSegment(segmentTheta, kOuterRadius);
|
|
addGearSegment(segmentTheta + kOuterRimEndAngle, kOuterRadius);
|
|
addGearSegment(segmentTheta + kInnerRimBeginAngle, kMiddleRadius);
|
|
addGearSegment(segmentTheta + (kAnglePerGearSection - kInnerRimBeginAngle), kMiddleRadius);
|
|
addGearSegment(segmentTheta + (kAnglePerGearSection - kOuterRimEndAngle), kOuterRadius);
|
|
}
|
|
|
|
self.gearVertexCount = (vertices.length / 2) - self.gearOffset;
|
|
|
|
// Build back arrow
|
|
self.arrowOffset = (vertices.length / 2);
|
|
|
|
function addArrowVertex(x, y) {
|
|
vertices.push(buttonBorder + x, gl.drawingBufferHeight - buttonBorder - y);
|
|
}
|
|
|
|
var angledLineWidth = lineWidth / Math.sin(45 * DEG2RAD);
|
|
|
|
addArrowVertex(0, buttonScale);
|
|
addArrowVertex(buttonScale, 0);
|
|
addArrowVertex(buttonScale + angledLineWidth, angledLineWidth);
|
|
addArrowVertex(angledLineWidth, buttonScale + angledLineWidth);
|
|
|
|
addArrowVertex(angledLineWidth, buttonScale - angledLineWidth);
|
|
addArrowVertex(0, buttonScale);
|
|
addArrowVertex(buttonScale, buttonScale * 2);
|
|
addArrowVertex(buttonScale + angledLineWidth, (buttonScale * 2) - angledLineWidth);
|
|
|
|
addArrowVertex(angledLineWidth, buttonScale - angledLineWidth);
|
|
addArrowVertex(0, buttonScale);
|
|
|
|
addArrowVertex(angledLineWidth, buttonScale - lineWidth);
|
|
addArrowVertex(kButtonWidthDp * dps, buttonScale - lineWidth);
|
|
addArrowVertex(angledLineWidth, buttonScale + lineWidth);
|
|
addArrowVertex(kButtonWidthDp * dps, buttonScale + lineWidth);
|
|
|
|
self.arrowVertexCount = (vertices.length / 2) - self.arrowOffset;
|
|
|
|
// Buffer data
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, self.vertexBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Performs distortion pass on the injected backbuffer, rendering it to the real
|
|
* backbuffer.
|
|
*/
|
|
CardboardUI.prototype.render = function() {
|
|
var gl = this.gl;
|
|
var self = this;
|
|
|
|
var glState = [
|
|
gl.CULL_FACE,
|
|
gl.DEPTH_TEST,
|
|
gl.BLEND,
|
|
gl.SCISSOR_TEST,
|
|
gl.STENCIL_TEST,
|
|
gl.COLOR_WRITEMASK,
|
|
gl.VIEWPORT,
|
|
|
|
gl.CURRENT_PROGRAM,
|
|
gl.ARRAY_BUFFER_BINDING
|
|
];
|
|
|
|
WGLUPreserveGLState(gl, glState, function(gl) {
|
|
// Make sure the GL state is in a good place
|
|
gl.disable(gl.CULL_FACE);
|
|
gl.disable(gl.DEPTH_TEST);
|
|
gl.disable(gl.BLEND);
|
|
gl.disable(gl.SCISSOR_TEST);
|
|
gl.disable(gl.STENCIL_TEST);
|
|
gl.colorMask(true, true, true, true);
|
|
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
|
|
self.renderNoState();
|
|
});
|
|
};
|
|
|
|
CardboardUI.prototype.renderNoState = function() {
|
|
var gl = this.gl;
|
|
|
|
// Bind distortion program and mesh
|
|
gl.useProgram(this.program);
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
|
|
gl.enableVertexAttribArray(this.attribs.position);
|
|
gl.vertexAttribPointer(this.attribs.position, 2, gl.FLOAT, false, 8, 0);
|
|
|
|
gl.uniform4f(this.uniforms.color, 1.0, 1.0, 1.0, 1.0);
|
|
|
|
Util.orthoMatrix(this.projMat, 0, gl.drawingBufferWidth, 0, gl.drawingBufferHeight, 0.1, 1024.0);
|
|
gl.uniformMatrix4fv(this.uniforms.projectionMat, false, this.projMat);
|
|
|
|
// Draws UI element
|
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
gl.drawArrays(gl.TRIANGLE_STRIP, this.gearOffset, this.gearVertexCount);
|
|
gl.drawArrays(gl.TRIANGLE_STRIP, this.arrowOffset, this.arrowVertexCount);
|
|
};
|
|
|
|
module.exports = CardboardUI;
|
|
|
|
},{"./deps/wglu-preserve-state.js":6,"./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 CardboardDistorter = _dereq_('./cardboard-distorter.js');
|
|
var CardboardUI = _dereq_('./cardboard-ui.js');
|
|
var DeviceInfo = _dereq_('./device-info.js');
|
|
var Dpdb = _dereq_('./dpdb/dpdb.js');
|
|
var FusionPoseSensor = _dereq_('./sensor-fusion/fusion-pose-sensor.js');
|
|
var RotateInstructions = _dereq_('./rotate-instructions.js');
|
|
var ViewerSelector = _dereq_('./viewer-selector.js');
|
|
var VRDisplay = _dereq_('./base.js').VRDisplay;
|
|
var Util = _dereq_('./util.js');
|
|
|
|
var Eye = {
|
|
LEFT: 'left',
|
|
RIGHT: 'right'
|
|
};
|
|
|
|
/**
|
|
* VRDisplay based on mobile device parameters and DeviceMotion APIs.
|
|
*/
|
|
function CardboardVRDisplay() {
|
|
this.displayName = 'Cardboard VRDisplay (webvr-polyfill)';
|
|
|
|
this.capabilities.hasOrientation = true;
|
|
this.capabilities.canPresent = true;
|
|
|
|
// "Private" members.
|
|
this.bufferScale_ = WebVRConfig.BUFFER_SCALE;
|
|
this.poseSensor_ = new FusionPoseSensor();
|
|
this.distorter_ = null;
|
|
this.cardboardUI_ = null;
|
|
|
|
this.dpdb_ = new Dpdb(true, this.onDeviceParamsUpdated_.bind(this));
|
|
this.deviceInfo_ = new DeviceInfo(this.dpdb_.getDeviceParams());
|
|
|
|
this.viewerSelector_ = new ViewerSelector();
|
|
this.viewerSelector_.on('change', this.onViewerChanged_.bind(this));
|
|
|
|
// Set the correct initial viewer.
|
|
this.deviceInfo_.setViewer(this.viewerSelector_.getCurrentViewer());
|
|
|
|
if (!WebVRConfig.ROTATE_INSTRUCTIONS_DISABLED) {
|
|
this.rotateInstructions_ = new RotateInstructions();
|
|
}
|
|
|
|
if (Util.isIOS()) {
|
|
// Listen for resize events to workaround this awful Safari bug.
|
|
window.addEventListener('resize', this.onResize_.bind(this));
|
|
}
|
|
}
|
|
CardboardVRDisplay.prototype = new VRDisplay();
|
|
|
|
CardboardVRDisplay.prototype.getImmediatePose = function() {
|
|
return {
|
|
position: this.poseSensor_.getPosition(),
|
|
orientation: this.poseSensor_.getOrientation(),
|
|
linearVelocity: null,
|
|
linearAcceleration: null,
|
|
angularVelocity: null,
|
|
angularAcceleration: null
|
|
};
|
|
};
|
|
|
|
CardboardVRDisplay.prototype.resetPose = function() {
|
|
this.poseSensor_.resetPose();
|
|
};
|
|
|
|
CardboardVRDisplay.prototype.getEyeParameters = function(whichEye) {
|
|
var offset = [this.deviceInfo_.viewer.interLensDistance * 0.5, 0.0, 0.0];
|
|
var fieldOfView;
|
|
|
|
// TODO: FoV can be a little expensive to compute. Cache when device params change.
|
|
if (whichEye == Eye.LEFT) {
|
|
offset[0] *= -1.0;
|
|
fieldOfView = this.deviceInfo_.getFieldOfViewLeftEye();
|
|
} else if (whichEye == Eye.RIGHT) {
|
|
fieldOfView = this.deviceInfo_.getFieldOfViewRightEye();
|
|
} else {
|
|
console.error('Invalid eye provided: %s', whichEye);
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
fieldOfView: fieldOfView,
|
|
offset: offset,
|
|
// TODO: Should be able to provide better values than these.
|
|
renderWidth: this.deviceInfo_.device.width * 0.5 * this.bufferScale_,
|
|
renderHeight: this.deviceInfo_.device.height * this.bufferScale_,
|
|
};
|
|
};
|
|
|
|
CardboardVRDisplay.prototype.onDeviceParamsUpdated_ = function(newParams) {
|
|
console.log('DPDB reported that device params were updated.');
|
|
this.deviceInfo_.updateDeviceParams(newParams);
|
|
|
|
if (this.distorter_) {
|
|
this.distorter.updateDeviceInfo(this.deviceInfo_);
|
|
}
|
|
};
|
|
|
|
CardboardVRDisplay.prototype.updateBounds_ = function () {
|
|
if (this.layer_ && this.distorter_ && (this.layer_.leftBounds || this.layer_.rightBounds)) {
|
|
this.distorter_.setTextureBounds(this.layer_.leftBounds, this.layer_.rightBounds);
|
|
}
|
|
};
|
|
|
|
CardboardVRDisplay.prototype.beginPresent_ = function() {
|
|
var gl = this.layer_.source.getContext('webgl');
|
|
if (!gl)
|
|
gl = this.layer_.source.getContext('experimental-webgl');
|
|
if (!gl)
|
|
gl = this.layer_.source.getContext('webgl2');
|
|
|
|
if (!gl)
|
|
return; // Can't do distortion without a WebGL context.
|
|
|
|
// Provides a way to opt out of distortion
|
|
if (this.layer_.predistorted) {
|
|
if (!WebVRConfig.CARDBOARD_UI_DISABLED) {
|
|
gl.canvas.width = Util.getScreenWidth() * this.bufferScale_;
|
|
gl.canvas.height = Util.getScreenHeight() * this.bufferScale_;
|
|
this.cardboardUI_ = new CardboardUI(gl);
|
|
}
|
|
} else {
|
|
// Create a new distorter for the target context
|
|
this.distorter_ = new CardboardDistorter(gl);
|
|
this.distorter_.updateDeviceInfo(this.deviceInfo_);
|
|
this.cardboardUI_ = this.distorter_.cardboardUI;
|
|
}
|
|
|
|
if (this.cardboardUI_) {
|
|
this.cardboardUI_.listen(function(e) {
|
|
// Options clicked.
|
|
this.viewerSelector_.show(this.layer_.source.parentElement);
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
}.bind(this), function(e) {
|
|
// Back clicked.
|
|
this.exitPresent();
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
}.bind(this));
|
|
}
|
|
|
|
if (this.rotateInstructions_) {
|
|
if (Util.isLandscapeMode() && Util.isMobile()) {
|
|
// In landscape mode, temporarily show the "put into Cardboard"
|
|
// interstitial. Otherwise, do the default thing.
|
|
this.rotateInstructions_.showTemporarily(3000, this.layer_.source.parentElement);
|
|
} else {
|
|
this.rotateInstructions_.update();
|
|
}
|
|
}
|
|
|
|
// Listen for orientation change events in order to show interstitial.
|
|
this.orientationHandler = this.onOrientationChange_.bind(this);
|
|
window.addEventListener('orientationchange', this.orientationHandler);
|
|
|
|
// Listen for present display change events in order to update distorter dimensions
|
|
this.vrdisplaypresentchangeHandler = this.updateBounds_.bind(this);
|
|
window.addEventListener('vrdisplaypresentchange', this.vrdisplaypresentchangeHandler);
|
|
|
|
// Fire this event initially, to give geometry-distortion clients the chance
|
|
// to do something custom.
|
|
this.fireVRDisplayDeviceParamsChange_();
|
|
};
|
|
|
|
CardboardVRDisplay.prototype.endPresent_ = function() {
|
|
if (this.distorter_) {
|
|
this.distorter_.destroy();
|
|
this.distorter_ = null;
|
|
}
|
|
if (this.cardboardUI_) {
|
|
this.cardboardUI_.destroy();
|
|
this.cardboardUI_ = null;
|
|
}
|
|
|
|
if (this.rotateInstructions_) {
|
|
this.rotateInstructions_.hide();
|
|
}
|
|
this.viewerSelector_.hide();
|
|
|
|
window.removeEventListener('orientationchange', this.orientationHandler);
|
|
window.removeEventListener('vrdisplaypresentchange', this.vrdisplaypresentchangeHandler);
|
|
};
|
|
|
|
CardboardVRDisplay.prototype.submitFrame = function(pose) {
|
|
if (this.distorter_) {
|
|
this.distorter_.submitFrame();
|
|
} else if (this.cardboardUI_ && this.layer_) {
|
|
// Hack for predistorted: true.
|
|
var canvas = this.layer_.source.getContext('webgl').canvas;
|
|
if (canvas.width != this.lastWidth || canvas.height != this.lastHeight) {
|
|
this.cardboardUI_.onResize();
|
|
}
|
|
this.lastWidth = canvas.width;
|
|
this.lastHeight = canvas.height;
|
|
|
|
// Render the Cardboard UI.
|
|
this.cardboardUI_.render();
|
|
}
|
|
};
|
|
|
|
CardboardVRDisplay.prototype.onOrientationChange_ = function(e) {
|
|
console.log('onOrientationChange_');
|
|
|
|
// Hide the viewer selector.
|
|
this.viewerSelector_.hide();
|
|
|
|
// Update the rotate instructions.
|
|
if (this.rotateInstructions_) {
|
|
this.rotateInstructions_.update();
|
|
}
|
|
|
|
this.onResize_();
|
|
};
|
|
|
|
CardboardVRDisplay.prototype.onResize_ = function(e) {
|
|
if (this.layer_) {
|
|
var gl = this.layer_.source.getContext('webgl');
|
|
// Size the CSS canvas.
|
|
// Added padding on right and bottom because iPhone 5 will not
|
|
// hide the URL bar unless content is bigger than the screen.
|
|
// This will not be visible as long as the container element (e.g. body)
|
|
// is set to 'overflow: hidden'.
|
|
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 10px 10px 0',
|
|
];
|
|
gl.canvas.setAttribute('style', cssProperties.join('; ') + ';');
|
|
|
|
Util.safariCssSizeWorkaround(gl.canvas);
|
|
}
|
|
};
|
|
|
|
CardboardVRDisplay.prototype.onViewerChanged_ = function(viewer) {
|
|
this.deviceInfo_.setViewer(viewer);
|
|
|
|
if (this.distorter_) {
|
|
// Update the distortion appropriately.
|
|
this.distorter_.updateDeviceInfo(this.deviceInfo_);
|
|
}
|
|
|
|
// Fire a new event containing viewer and device parameters for clients that
|
|
// want to implement their own geometry-based distortion.
|
|
this.fireVRDisplayDeviceParamsChange_();
|
|
};
|
|
|
|
CardboardVRDisplay.prototype.fireVRDisplayDeviceParamsChange_ = function() {
|
|
var event = new CustomEvent('vrdisplaydeviceparamschange', {
|
|
detail: {
|
|
vrdisplay: this,
|
|
deviceInfo: this.deviceInfo_,
|
|
}
|
|
});
|
|
window.dispatchEvent(event);
|
|
};
|
|
|
|
module.exports = CardboardVRDisplay;
|
|
|
|
},{"./base.js":2,"./cardboard-distorter.js":3,"./cardboard-ui.js":4,"./device-info.js":7,"./dpdb/dpdb.js":11,"./rotate-instructions.js":16,"./sensor-fusion/fusion-pose-sensor.js":18,"./util.js":22,"./viewer-selector.js":23}],6:[function(_dereq_,module,exports){
|
|
/*
|
|
Copyright (c) 2016, Brandon Jones.
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
*/
|
|
|
|
/*
|
|
Caches specified GL state, runs a callback, and restores the cached state when
|
|
done.
|
|
|
|
Example usage:
|
|
|
|
var savedState = [
|
|
gl.ARRAY_BUFFER_BINDING,
|
|
|
|
// TEXTURE_BINDING_2D or _CUBE_MAP must always be followed by the texure unit.
|
|
gl.TEXTURE_BINDING_2D, gl.TEXTURE0,
|
|
|
|
gl.CLEAR_COLOR,
|
|
];
|
|
// After this call the array buffer, texture unit 0, active texture, and clear
|
|
// color will be restored. The viewport will remain changed, however, because
|
|
// gl.VIEWPORT was not included in the savedState list.
|
|
WGLUPreserveGLState(gl, savedState, function(gl) {
|
|
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
|
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, ....);
|
|
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
gl.texImage2D(gl.TEXTURE_2D, ...);
|
|
|
|
gl.clearColor(1, 0, 0, 1);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
});
|
|
|
|
Note that this is not intended to be fast. Managing state in your own code to
|
|
avoid redundant state setting and querying will always be faster. This function
|
|
is most useful for cases where you may not have full control over the WebGL
|
|
calls being made, such as tooling or effect injectors.
|
|
*/
|
|
|
|
function WGLUPreserveGLState(gl, bindings, callback) {
|
|
if (!bindings) {
|
|
callback(gl);
|
|
return;
|
|
}
|
|
|
|
var boundValues = [];
|
|
|
|
var activeTexture = null;
|
|
for (var i = 0; i < bindings.length; ++i) {
|
|
var binding = bindings[i];
|
|
switch (binding) {
|
|
case gl.TEXTURE_BINDING_2D:
|
|
case gl.TEXTURE_BINDING_CUBE_MAP:
|
|
var textureUnit = bindings[++i];
|
|
if (textureUnit < gl.TEXTURE0 || textureUnit > gl.TEXTURE31) {
|
|
console.error("TEXTURE_BINDING_2D or TEXTURE_BINDING_CUBE_MAP must be followed by a valid texture unit");
|
|
boundValues.push(null, null);
|
|
break;
|
|
}
|
|
if (!activeTexture) {
|
|
activeTexture = gl.getParameter(gl.ACTIVE_TEXTURE);
|
|
}
|
|
gl.activeTexture(textureUnit);
|
|
boundValues.push(gl.getParameter(binding), null);
|
|
break;
|
|
case gl.ACTIVE_TEXTURE:
|
|
activeTexture = gl.getParameter(gl.ACTIVE_TEXTURE);
|
|
boundValues.push(null);
|
|
break;
|
|
default:
|
|
boundValues.push(gl.getParameter(binding));
|
|
break;
|
|
}
|
|
}
|
|
|
|
callback(gl);
|
|
|
|
for (var i = 0; i < bindings.length; ++i) {
|
|
var binding = bindings[i];
|
|
var boundValue = boundValues[i];
|
|
switch (binding) {
|
|
case gl.ACTIVE_TEXTURE:
|
|
break; // Ignore this binding, since we special-case it to happen last.
|
|
case gl.ARRAY_BUFFER_BINDING:
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, boundValue);
|
|
break;
|
|
case gl.COLOR_CLEAR_VALUE:
|
|
gl.clearColor(boundValue[0], boundValue[1], boundValue[2], boundValue[3]);
|
|
break;
|
|
case gl.COLOR_WRITEMASK:
|
|
gl.colorMask(boundValue[0], boundValue[1], boundValue[2], boundValue[3]);
|
|
break;
|
|
case gl.CURRENT_PROGRAM:
|
|
gl.useProgram(boundValue);
|
|
break;
|
|
case gl.ELEMENT_ARRAY_BUFFER_BINDING:
|
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, boundValue);
|
|
break;
|
|
case gl.FRAMEBUFFER_BINDING:
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, boundValue);
|
|
break;
|
|
case gl.RENDERBUFFER_BINDING:
|
|
gl.bindRenderbuffer(gl.RENDERBUFFER, boundValue);
|
|
break;
|
|
case gl.TEXTURE_BINDING_2D:
|
|
var textureUnit = bindings[++i];
|
|
if (textureUnit < gl.TEXTURE0 || textureUnit > gl.TEXTURE31)
|
|
break;
|
|
gl.activeTexture(textureUnit);
|
|
gl.bindTexture(gl.TEXTURE_2D, boundValue);
|
|
break;
|
|
case gl.TEXTURE_BINDING_CUBE_MAP:
|
|
var textureUnit = bindings[++i];
|
|
if (textureUnit < gl.TEXTURE0 || textureUnit > gl.TEXTURE31)
|
|
break;
|
|
gl.activeTexture(textureUnit);
|
|
gl.bindTexture(gl.TEXTURE_CUBE_MAP, boundValue);
|
|
break;
|
|
case gl.VIEWPORT:
|
|
gl.viewport(boundValue[0], boundValue[1], boundValue[2], boundValue[3]);
|
|
break;
|
|
case gl.BLEND:
|
|
case gl.CULL_FACE:
|
|
case gl.DEPTH_TEST:
|
|
case gl.SCISSOR_TEST:
|
|
case gl.STENCIL_TEST:
|
|
if (boundValue) {
|
|
gl.enable(binding);
|
|
} else {
|
|
gl.disable(binding);
|
|
}
|
|
break;
|
|
default:
|
|
console.log("No GL restore behavior for 0x" + binding.toString(16));
|
|
break;
|
|
}
|
|
|
|
if (activeTexture) {
|
|
gl.activeTexture(activeTexture);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = WGLUPreserveGLState;
|
|
},{}],7:[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 Distortion = _dereq_('./distortion/distortion.js');
|
|
var MathUtil = _dereq_('./math-util.js');
|
|
var Util = _dereq_('./util.js');
|
|
|
|
function Device(params) {
|
|
this.width = params.width || Util.getScreenWidth();
|
|
this.height = params.height || Util.getScreenHeight();
|
|
this.widthMeters = params.widthMeters;
|
|
this.heightMeters = params.heightMeters;
|
|
this.bevelMeters = params.bevelMeters;
|
|
}
|
|
|
|
|
|
// Fallback Android device (based on Nexus 5 measurements) for use when
|
|
// we can't recognize an Android device.
|
|
var DEFAULT_ANDROID = new Device({
|
|
widthMeters: 0.110,
|
|
heightMeters: 0.062,
|
|
bevelMeters: 0.004
|
|
});
|
|
|
|
// Fallback iOS device (based on iPhone6) for use when
|
|
// we can't recognize an Android device.
|
|
var DEFAULT_IOS = new Device({
|
|
widthMeters: 0.1038,
|
|
heightMeters: 0.0584,
|
|
bevelMeters: 0.004
|
|
});
|
|
|
|
|
|
var Viewers = {
|
|
CardboardV1: new CardboardViewer({
|
|
id: 'CardboardV1',
|
|
label: 'Cardboard I/O 2014',
|
|
fov: 40,
|
|
interLensDistance: 0.060,
|
|
baselineLensDistance: 0.035,
|
|
screenLensDistance: 0.042,
|
|
distortionCoefficients: [0.441, 0.156],
|
|
inverseCoefficients: [-0.4410035, 0.42756155, -0.4804439, 0.5460139,
|
|
-0.58821183, 0.5733938, -0.48303202, 0.33299083, -0.17573841,
|
|
0.0651772, -0.01488963, 0.001559834]
|
|
}),
|
|
CardboardV2: new CardboardViewer({
|
|
id: 'CardboardV2',
|
|
label: 'Cardboard I/O 2015',
|
|
fov: 60,
|
|
interLensDistance: 0.064,
|
|
baselineLensDistance: 0.035,
|
|
screenLensDistance: 0.039,
|
|
distortionCoefficients: [0.34, 0.55],
|
|
inverseCoefficients: [-0.33836704, -0.18162185, 0.862655, -1.2462051,
|
|
1.0560602, -0.58208317, 0.21609078, -0.05444823, 0.009177956,
|
|
-9.904169E-4, 6.183535E-5, -1.6981803E-6]
|
|
})
|
|
};
|
|
|
|
|
|
var DEFAULT_LEFT_CENTER = {x: 0.5, y: 0.5};
|
|
var DEFAULT_RIGHT_CENTER = {x: 0.5, y: 0.5};
|
|
|
|
/**
|
|
* Manages information about the device and the viewer.
|
|
*
|
|
* deviceParams indicates the parameters of the device to use (generally
|
|
* obtained from dpdb.getDeviceParams()). Can be null to mean no device
|
|
* params were found.
|
|
*/
|
|
function DeviceInfo(deviceParams) {
|
|
this.viewer = Viewers.CardboardV2;
|
|
this.updateDeviceParams(deviceParams);
|
|
this.distortion = new Distortion(this.viewer.distortionCoefficients);
|
|
}
|
|
|
|
DeviceInfo.prototype.updateDeviceParams = function(deviceParams) {
|
|
this.device = this.determineDevice_(deviceParams) || this.device;
|
|
};
|
|
|
|
DeviceInfo.prototype.getDevice = function() {
|
|
return this.device;
|
|
};
|
|
|
|
DeviceInfo.prototype.setViewer = function(viewer) {
|
|
this.viewer = viewer;
|
|
this.distortion = new Distortion(this.viewer.distortionCoefficients);
|
|
};
|
|
|
|
DeviceInfo.prototype.determineDevice_ = function(deviceParams) {
|
|
if (!deviceParams) {
|
|
// No parameters, so use a default.
|
|
if (Util.isIOS()) {
|
|
console.warn('Using fallback iOS device measurements.');
|
|
return DEFAULT_IOS;
|
|
} else {
|
|
console.warn('Using fallback Android device measurements.');
|
|
return DEFAULT_ANDROID;
|
|
}
|
|
}
|
|
|
|
// Compute device screen dimensions based on deviceParams.
|
|
var METERS_PER_INCH = 0.0254;
|
|
var metersPerPixelX = METERS_PER_INCH / deviceParams.xdpi;
|
|
var metersPerPixelY = METERS_PER_INCH / deviceParams.ydpi;
|
|
var width = Util.getScreenWidth();
|
|
var height = Util.getScreenHeight();
|
|
return new Device({
|
|
widthMeters: metersPerPixelX * width,
|
|
heightMeters: metersPerPixelY * height,
|
|
bevelMeters: deviceParams.bevelMm * 0.001,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Calculates field of view for the left eye.
|
|
*/
|
|
DeviceInfo.prototype.getDistortedFieldOfViewLeftEye = function() {
|
|
var viewer = this.viewer;
|
|
var device = this.device;
|
|
var distortion = this.distortion;
|
|
|
|
// Device.height and device.width for device in portrait mode, so transpose.
|
|
var eyeToScreenDistance = viewer.screenLensDistance;
|
|
|
|
var outerDist = (device.widthMeters - viewer.interLensDistance) / 2;
|
|
var innerDist = viewer.interLensDistance / 2;
|
|
var bottomDist = viewer.baselineLensDistance - device.bevelMeters;
|
|
var topDist = device.heightMeters - bottomDist;
|
|
|
|
var outerAngle = MathUtil.radToDeg * Math.atan(
|
|
distortion.distort(outerDist / eyeToScreenDistance));
|
|
var innerAngle = MathUtil.radToDeg * Math.atan(
|
|
distortion.distort(innerDist / eyeToScreenDistance));
|
|
var bottomAngle = MathUtil.radToDeg * Math.atan(
|
|
distortion.distort(bottomDist / eyeToScreenDistance));
|
|
var topAngle = MathUtil.radToDeg * Math.atan(
|
|
distortion.distort(topDist / eyeToScreenDistance));
|
|
|
|
return {
|
|
leftDegrees: Math.min(outerAngle, viewer.fov),
|
|
rightDegrees: Math.min(innerAngle, viewer.fov),
|
|
downDegrees: Math.min(bottomAngle, viewer.fov),
|
|
upDegrees: Math.min(topAngle, viewer.fov)
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Calculates the tan-angles from the maximum FOV for the left eye for the
|
|
* current device and screen parameters.
|
|
*/
|
|
DeviceInfo.prototype.getLeftEyeVisibleTanAngles = function() {
|
|
var viewer = this.viewer;
|
|
var device = this.device;
|
|
var distortion = this.distortion;
|
|
|
|
// Tan-angles from the max FOV.
|
|
var fovLeft = Math.tan(-MathUtil.degToRad * viewer.fov);
|
|
var fovTop = Math.tan(MathUtil.degToRad * viewer.fov);
|
|
var fovRight = Math.tan(MathUtil.degToRad * viewer.fov);
|
|
var fovBottom = Math.tan(-MathUtil.degToRad * viewer.fov);
|
|
// Viewport size.
|
|
var halfWidth = device.widthMeters / 4;
|
|
var halfHeight = device.heightMeters / 2;
|
|
// Viewport center, measured from left lens position.
|
|
var verticalLensOffset = (viewer.baselineLensDistance - device.bevelMeters - halfHeight);
|
|
var centerX = viewer.interLensDistance / 2 - halfWidth;
|
|
var centerY = -verticalLensOffset;
|
|
var centerZ = viewer.screenLensDistance;
|
|
// Tan-angles of the viewport edges, as seen through the lens.
|
|
var screenLeft = distortion.distort((centerX - halfWidth) / centerZ);
|
|
var screenTop = distortion.distort((centerY + halfHeight) / centerZ);
|
|
var screenRight = distortion.distort((centerX + halfWidth) / centerZ);
|
|
var screenBottom = distortion.distort((centerY - halfHeight) / centerZ);
|
|
// Compare the two sets of tan-angles and take the value closer to zero on each side.
|
|
var result = new Float32Array(4);
|
|
result[0] = Math.max(fovLeft, screenLeft);
|
|
result[1] = Math.min(fovTop, screenTop);
|
|
result[2] = Math.min(fovRight, screenRight);
|
|
result[3] = Math.max(fovBottom, screenBottom);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Calculates the tan-angles from the maximum FOV for the left eye for the
|
|
* current device and screen parameters, assuming no lenses.
|
|
*/
|
|
DeviceInfo.prototype.getLeftEyeNoLensTanAngles = function() {
|
|
var viewer = this.viewer;
|
|
var device = this.device;
|
|
var distortion = this.distortion;
|
|
|
|
var result = new Float32Array(4);
|
|
// Tan-angles from the max FOV.
|
|
var fovLeft = distortion.distortInverse(Math.tan(-MathUtil.degToRad * viewer.fov));
|
|
var fovTop = distortion.distortInverse(Math.tan(MathUtil.degToRad * viewer.fov));
|
|
var fovRight = distortion.distortInverse(Math.tan(MathUtil.degToRad * viewer.fov));
|
|
var fovBottom = distortion.distortInverse(Math.tan(-MathUtil.degToRad * viewer.fov));
|
|
// Viewport size.
|
|
var halfWidth = device.widthMeters / 4;
|
|
var halfHeight = device.heightMeters / 2;
|
|
// Viewport center, measured from left lens position.
|
|
var verticalLensOffset = (viewer.baselineLensDistance - device.bevelMeters - halfHeight);
|
|
var centerX = viewer.interLensDistance / 2 - halfWidth;
|
|
var centerY = -verticalLensOffset;
|
|
var centerZ = viewer.screenLensDistance;
|
|
// Tan-angles of the viewport edges, as seen through the lens.
|
|
var screenLeft = (centerX - halfWidth) / centerZ;
|
|
var screenTop = (centerY + halfHeight) / centerZ;
|
|
var screenRight = (centerX + halfWidth) / centerZ;
|
|
var screenBottom = (centerY - halfHeight) / centerZ;
|
|
// Compare the two sets of tan-angles and take the value closer to zero on each side.
|
|
result[0] = Math.max(fovLeft, screenLeft);
|
|
result[1] = Math.min(fovTop, screenTop);
|
|
result[2] = Math.min(fovRight, screenRight);
|
|
result[3] = Math.max(fovBottom, screenBottom);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Calculates the screen rectangle visible from the left eye for the
|
|
* current device and screen parameters.
|
|
*/
|
|
DeviceInfo.prototype.getLeftEyeVisibleScreenRect = function(undistortedFrustum) {
|
|
var viewer = this.viewer;
|
|
var device = this.device;
|
|
|
|
var dist = viewer.screenLensDistance;
|
|
var eyeX = (device.widthMeters - viewer.interLensDistance) / 2;
|
|
var eyeY = viewer.baselineLensDistance - device.bevelMeters;
|
|
var left = (undistortedFrustum[0] * dist + eyeX) / device.widthMeters;
|
|
var top = (undistortedFrustum[1] * dist + eyeY) / device.heightMeters;
|
|
var right = (undistortedFrustum[2] * dist + eyeX) / device.widthMeters;
|
|
var bottom = (undistortedFrustum[3] * dist + eyeY) / device.heightMeters;
|
|
return {
|
|
x: left,
|
|
y: bottom,
|
|
width: right - left,
|
|
height: top - bottom
|
|
};
|
|
};
|
|
|
|
DeviceInfo.prototype.getFieldOfViewLeftEye = function(opt_isUndistorted) {
|
|
return opt_isUndistorted ? this.getUndistortedFieldOfViewLeftEye() :
|
|
this.getDistortedFieldOfViewLeftEye();
|
|
};
|
|
|
|
DeviceInfo.prototype.getFieldOfViewRightEye = function(opt_isUndistorted) {
|
|
var fov = this.getFieldOfViewLeftEye(opt_isUndistorted);
|
|
return {
|
|
leftDegrees: fov.rightDegrees,
|
|
rightDegrees: fov.leftDegrees,
|
|
upDegrees: fov.upDegrees,
|
|
downDegrees: fov.downDegrees
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Calculates undistorted field of view for the left eye.
|
|
*/
|
|
DeviceInfo.prototype.getUndistortedFieldOfViewLeftEye = function() {
|
|
var p = this.getUndistortedParams_();
|
|
|
|
return {
|
|
leftDegrees: MathUtil.radToDeg * Math.atan(p.outerDist),
|
|
rightDegrees: MathUtil.radToDeg * Math.atan(p.innerDist),
|
|
downDegrees: MathUtil.radToDeg * Math.atan(p.bottomDist),
|
|
upDegrees: MathUtil.radToDeg * Math.atan(p.topDist)
|
|
};
|
|
};
|
|
|
|
DeviceInfo.prototype.getUndistortedViewportLeftEye = function() {
|
|
var p = this.getUndistortedParams_();
|
|
var viewer = this.viewer;
|
|
var device = this.device;
|
|
|
|
// Distances stored in local variables are in tan-angle units unless otherwise
|
|
// noted.
|
|
var eyeToScreenDistance = viewer.screenLensDistance;
|
|
var screenWidth = device.widthMeters / eyeToScreenDistance;
|
|
var screenHeight = device.heightMeters / eyeToScreenDistance;
|
|
var xPxPerTanAngle = device.width / screenWidth;
|
|
var yPxPerTanAngle = device.height / screenHeight;
|
|
|
|
var x = Math.round((p.eyePosX - p.outerDist) * xPxPerTanAngle);
|
|
var y = Math.round((p.eyePosY - p.bottomDist) * yPxPerTanAngle);
|
|
return {
|
|
x: x,
|
|
y: y,
|
|
width: Math.round((p.eyePosX + p.innerDist) * xPxPerTanAngle) - x,
|
|
height: Math.round((p.eyePosY + p.topDist) * yPxPerTanAngle) - y
|
|
};
|
|
};
|
|
|
|
DeviceInfo.prototype.getUndistortedParams_ = function() {
|
|
var viewer = this.viewer;
|
|
var device = this.device;
|
|
var distortion = this.distortion;
|
|
|
|
// Most of these variables in tan-angle units.
|
|
var eyeToScreenDistance = viewer.screenLensDistance;
|
|
var halfLensDistance = viewer.interLensDistance / 2 / eyeToScreenDistance;
|
|
var screenWidth = device.widthMeters / eyeToScreenDistance;
|
|
var screenHeight = device.heightMeters / eyeToScreenDistance;
|
|
|
|
var eyePosX = screenWidth / 2 - halfLensDistance;
|
|
var eyePosY = (viewer.baselineLensDistance - device.bevelMeters) / eyeToScreenDistance;
|
|
|
|
var maxFov = viewer.fov;
|
|
var viewerMax = distortion.distortInverse(Math.tan(MathUtil.degToRad * maxFov));
|
|
var outerDist = Math.min(eyePosX, viewerMax);
|
|
var innerDist = Math.min(halfLensDistance, viewerMax);
|
|
var bottomDist = Math.min(eyePosY, viewerMax);
|
|
var topDist = Math.min(screenHeight - eyePosY, viewerMax);
|
|
|
|
return {
|
|
outerDist: outerDist,
|
|
innerDist: innerDist,
|
|
topDist: topDist,
|
|
bottomDist: bottomDist,
|
|
eyePosX: eyePosX,
|
|
eyePosY: eyePosY
|
|
};
|
|
};
|
|
|
|
|
|
function CardboardViewer(params) {
|
|
// A machine readable ID.
|
|
this.id = params.id;
|
|
// A human readable label.
|
|
this.label = params.label;
|
|
|
|
// Field of view in degrees (per side).
|
|
this.fov = params.fov;
|
|
|
|
// Distance between lens centers in meters.
|
|
this.interLensDistance = params.interLensDistance;
|
|
// Distance between viewer baseline and lens center in meters.
|
|
this.baselineLensDistance = params.baselineLensDistance;
|
|
// Screen-to-lens distance in meters.
|
|
this.screenLensDistance = params.screenLensDistance;
|
|
|
|
// Distortion coefficients.
|
|
this.distortionCoefficients = params.distortionCoefficients;
|
|
// Inverse distortion coefficients.
|
|
// TODO: Calculate these from distortionCoefficients in the future.
|
|
this.inverseCoefficients = params.inverseCoefficients;
|
|
}
|
|
|
|
// Export viewer information.
|
|
DeviceInfo.Viewers = Viewers;
|
|
module.exports = DeviceInfo;
|
|
|
|
},{"./distortion/distortion.js":9,"./math-util.js":14,"./util.js":22}],8:[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 VRDisplay = _dereq_('./base.js').VRDisplay;
|
|
var HMDVRDevice = _dereq_('./base.js').HMDVRDevice;
|
|
var PositionSensorVRDevice = _dereq_('./base.js').PositionSensorVRDevice;
|
|
|
|
/**
|
|
* Wraps a VRDisplay and exposes it as a HMDVRDevice
|
|
*/
|
|
function VRDisplayHMDDevice(display) {
|
|
this.display = display;
|
|
|
|
this.hardwareUnitId = display.displayId;
|
|
this.deviceId = 'webvr-polyfill:HMD:' + display.displayId;
|
|
this.deviceName = display.displayName + ' (HMD)';
|
|
}
|
|
VRDisplayHMDDevice.prototype = new HMDVRDevice();
|
|
|
|
VRDisplayHMDDevice.prototype.getEyeParameters = function(whichEye) {
|
|
var eyeParameters = this.display.getEyeParameters(whichEye);
|
|
|
|
return {
|
|
currentFieldOfView: eyeParameters.fieldOfView,
|
|
maximumFieldOfView: eyeParameters.fieldOfView,
|
|
minimumFieldOfView: eyeParameters.fieldOfView,
|
|
recommendedFieldOfView: eyeParameters.fieldOfView,
|
|
eyeTranslation: { x: eyeParameters.offset[0], y: eyeParameters.offset[1], z: eyeParameters.offset[2] },
|
|
renderRect: {
|
|
x: (whichEye == 'right') ? eyeParameters.renderWidth : 0,
|
|
y: 0,
|
|
width: eyeParameters.renderWidth,
|
|
height: eyeParameters.renderHeight
|
|
}
|
|
};
|
|
};
|
|
|
|
VRDisplayHMDDevice.prototype.setFieldOfView =
|
|
function(opt_fovLeft, opt_fovRight, opt_zNear, opt_zFar) {
|
|
// Not supported. getEyeParameters reports that the min, max, and recommended
|
|
// FoV is all the same, so no adjustment can be made.
|
|
};
|
|
|
|
// TODO: Need to hook requestFullscreen to see if a wrapped VRDisplay was passed
|
|
// in as an option. If so we should prevent the default fullscreen behavior and
|
|
// call VRDisplay.requestPresent instead.
|
|
|
|
/**
|
|
* Wraps a VRDisplay and exposes it as a PositionSensorVRDevice
|
|
*/
|
|
function VRDisplayPositionSensorDevice(display) {
|
|
this.display = display;
|
|
|
|
this.hardwareUnitId = display.displayId;
|
|
this.deviceId = 'webvr-polyfill:PositionSensor: ' + display.displayId;
|
|
this.deviceName = display.displayName + ' (PositionSensor)';
|
|
}
|
|
VRDisplayPositionSensorDevice.prototype = new PositionSensorVRDevice();
|
|
|
|
VRDisplayPositionSensorDevice.prototype.getState = function() {
|
|
var pose = this.display.getPose();
|
|
return {
|
|
position: pose.position ? { x: pose.position[0], y: pose.position[1], z: pose.position[2] } : null,
|
|
orientation: pose.orientation ? { x: pose.orientation[0], y: pose.orientation[1], z: pose.orientation[2], w: pose.orientation[3] } : null,
|
|
linearVelocity: null,
|
|
linearAcceleration: null,
|
|
angularVelocity: null,
|
|
angularAcceleration: null
|
|
};
|
|
};
|
|
|
|
VRDisplayPositionSensorDevice.prototype.resetState = function() {
|
|
return this.positionDevice.resetPose();
|
|
};
|
|
|
|
|
|
module.exports.VRDisplayHMDDevice = VRDisplayHMDDevice;
|
|
module.exports.VRDisplayPositionSensorDevice = VRDisplayPositionSensorDevice;
|
|
|
|
|
|
},{"./base.js":2}],9:[function(_dereq_,module,exports){
|
|
/**
|
|
* TODO(smus): Implement coefficient inversion.
|
|
*/
|
|
function Distortion(coefficients) {
|
|
this.coefficients = coefficients;
|
|
}
|
|
|
|
/**
|
|
* Calculates the inverse distortion for a radius.
|
|
* </p><p>
|
|
* Allows to compute the original undistorted radius from a distorted one.
|
|
* See also getApproximateInverseDistortion() for a faster but potentially
|
|
* less accurate method.
|
|
*
|
|
* @param {Number} radius Distorted radius from the lens center in tan-angle units.
|
|
* @return {Number} The undistorted radius in tan-angle units.
|
|
*/
|
|
Distortion.prototype.distortInverse = function(radius) {
|
|
// Secant method.
|
|
var r0 = 0;
|
|
var r1 = 1;
|
|
var dr0 = radius - this.distort(r0);
|
|
while (Math.abs(r1 - r0) > 0.0001 /** 0.1mm */) {
|
|
var dr1 = radius - this.distort(r1);
|
|
var r2 = r1 - dr1 * ((r1 - r0) / (dr1 - dr0));
|
|
r0 = r1;
|
|
r1 = r2;
|
|
dr0 = dr1;
|
|
}
|
|
return r1;
|
|
};
|
|
|
|
/**
|
|
* Distorts a radius by its distortion factor from the center of the lenses.
|
|
*
|
|
* @param {Number} radius Radius from the lens center in tan-angle units.
|
|
* @return {Number} The distorted radius in tan-angle units.
|
|
*/
|
|
Distortion.prototype.distort = function(radius) {
|
|
var r2 = radius * radius;
|
|
var ret = 0;
|
|
for (var i = 0; i < this.coefficients.length; i++) {
|
|
ret = r2 * (ret + this.coefficients[i]);
|
|
}
|
|
return (ret + 1) * radius;
|
|
};
|
|
|
|
// Functions below roughly ported from
|
|
// https://github.com/googlesamples/cardboard-unity/blob/master/Cardboard/Scripts/CardboardProfile.cs#L412
|
|
|
|
// Solves a small linear equation via destructive gaussian
|
|
// elimination and back substitution. This isn't generic numeric
|
|
// code, it's just a quick hack to work with the generally
|
|
// well-behaved symmetric matrices for least-squares fitting.
|
|
// Not intended for reuse.
|
|
//
|
|
// @param a Input positive definite symmetrical matrix. Destroyed
|
|
// during calculation.
|
|
// @param y Input right-hand-side values. Destroyed during calculation.
|
|
// @return Resulting x value vector.
|
|
//
|
|
Distortion.prototype.solveLinear_ = function(a, y) {
|
|
var n = a.length;
|
|
|
|
// Gaussian elimination (no row exchange) to triangular matrix.
|
|
// The input matrix is a A^T A product which should be a positive
|
|
// definite symmetrical matrix, and if I remember my linear
|
|
// algebra right this implies that the pivots will be nonzero and
|
|
// calculations sufficiently accurate without needing row
|
|
// exchange.
|
|
for (var j = 0; j < n - 1; ++j) {
|
|
for (var k = j + 1; k < n; ++k) {
|
|
var p = a[j][k] / a[j][j];
|
|
for (var i = j + 1; i < n; ++i) {
|
|
a[i][k] -= p * a[i][j];
|
|
}
|
|
y[k] -= p * y[j];
|
|
}
|
|
}
|
|
// From this point on, only the matrix elements a[j][i] with i>=j are
|
|
// valid. The elimination doesn't fill in eliminated 0 values.
|
|
|
|
var x = new Array(n);
|
|
|
|
// Back substitution.
|
|
for (var j = n - 1; j >= 0; --j) {
|
|
var v = y[j];
|
|
for (var i = j + 1; i < n; ++i) {
|
|
v -= a[i][j] * x[i];
|
|
}
|
|
x[j] = v / a[j][j];
|
|
}
|
|
|
|
return x;
|
|
};
|
|
|
|
// Solves a least-squares matrix equation. Given the equation A * x = y, calculate the
|
|
// least-square fit x = inverse(A * transpose(A)) * transpose(A) * y. The way this works
|
|
// is that, while A is typically not a square matrix (and hence not invertible), A * transpose(A)
|
|
// is always square. That is:
|
|
// A * x = y
|
|
// transpose(A) * (A * x) = transpose(A) * y <- multiply both sides by transpose(A)
|
|
// (transpose(A) * A) * x = transpose(A) * y <- associativity
|
|
// x = inverse(transpose(A) * A) * transpose(A) * y <- solve for x
|
|
// Matrix A's row count (first index) must match y's value count. A's column count (second index)
|
|
// determines the length of the result vector x.
|
|
Distortion.prototype.solveLeastSquares_ = function(matA, vecY) {
|
|
var i, j, k, sum;
|
|
var numSamples = matA.length;
|
|
var numCoefficients = matA[0].length;
|
|
if (numSamples != vecY.Length) {
|
|
throw new Error("Matrix / vector dimension mismatch");
|
|
}
|
|
|
|
// Calculate transpose(A) * A
|
|
var matATA = new Array(numCoefficients);
|
|
for (k = 0; k < numCoefficients; ++k) {
|
|
matATA[k] = new Array(numCoefficients);
|
|
for (j = 0; j < numCoefficients; ++j) {
|
|
sum = 0;
|
|
for (i = 0; i < numSamples; ++i) {
|
|
sum += matA[j][i] * matA[k][i];
|
|
}
|
|
matATA[k][j] = sum;
|
|
}
|
|
}
|
|
|
|
// Calculate transpose(A) * y
|
|
var vecATY = new Array(numCoefficients);
|
|
for (j = 0; j < numCoefficients; ++j) {
|
|
sum = 0;
|
|
for (i = 0; i < numSamples; ++i) {
|
|
sum += matA[j][i] * vecY[i];
|
|
}
|
|
vecATY[j] = sum;
|
|
}
|
|
|
|
// Now solve (A * transpose(A)) * x = transpose(A) * y.
|
|
return this.solveLinear_(matATA, vecATY);
|
|
};
|
|
|
|
/// Calculates an approximate inverse to the given radial distortion parameters.
|
|
Distortion.prototype.approximateInverse = function(maxRadius, numSamples) {
|
|
maxRadius = maxRadius || 1;
|
|
numSamples = numSamples || 100;
|
|
var numCoefficients = 6;
|
|
var i, j;
|
|
|
|
// R + K1*R^3 + K2*R^5 = r, with R = rp = distort(r)
|
|
// Repeating for numSamples:
|
|
// [ R0^3, R0^5 ] * [ K1 ] = [ r0 - R0 ]
|
|
// [ R1^3, R1^5 ] [ K2 ] [ r1 - R1 ]
|
|
// [ R2^3, R2^5 ] [ r2 - R2 ]
|
|
// [ etc... ] [ etc... ]
|
|
// That is:
|
|
// matA * [K1, K2] = y
|
|
// Solve:
|
|
// [K1, K2] = inverse(transpose(matA) * matA) * transpose(matA) * y
|
|
var matA = new Array(numCoefficients);
|
|
for (j = 0; j < numCoefficients; ++j) {
|
|
matA[j] = new Array(numSamples);
|
|
}
|
|
var vecY = new Array(numSamples);
|
|
|
|
for (i = 0; i < numSamples; ++i) {
|
|
var r = maxRadius * (i + 1) / numSamples;
|
|
var rp = this.distort(r);
|
|
var v = rp;
|
|
for (j = 0; j < numCoefficients; ++j) {
|
|
v *= rp * rp;
|
|
matA[j][i] = v;
|
|
}
|
|
vecY[i] = r - rp;
|
|
}
|
|
|
|
var inverseCoefficients = this.solveLeastSquares_(matA, vecY);
|
|
|
|
return new Distortion(inverseCoefficients);
|
|
};
|
|
|
|
module.exports = Distortion;
|
|
|
|
},{}],10:[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.
|
|
*/
|
|
|
|
/**
|
|
* DPDB cache.
|
|
*/
|
|
var DPDB_CACHE = {
|
|
"format": 1,
|
|
"last_updated": "2016-01-20T00:18:35Z",
|
|
"devices": [
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "asus/*/Nexus 7/*" },
|
|
{ "ua": "Nexus 7" }
|
|
],
|
|
"dpi": [ 320.8, 323.0 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "asus/*/ASUS_Z00AD/*" },
|
|
{ "ua": "ASUS_Z00AD" }
|
|
],
|
|
"dpi": [ 403.0, 404.6 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "HTC/*/HTC6435LVW/*" },
|
|
{ "ua": "HTC6435LVW" }
|
|
],
|
|
"dpi": [ 449.7, 443.3 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "HTC/*/HTC One XL/*" },
|
|
{ "ua": "HTC One XL" }
|
|
],
|
|
"dpi": [ 315.3, 314.6 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "htc/*/Nexus 9/*" },
|
|
{ "ua": "Nexus 9" }
|
|
],
|
|
"dpi": 289.0,
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "HTC/*/HTC One M9/*" },
|
|
{ "ua": "HTC One M9" }
|
|
],
|
|
"dpi": [ 442.5, 443.3 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "HTC/*/HTC One_M8/*" },
|
|
{ "ua": "HTC One_M8" }
|
|
],
|
|
"dpi": [ 449.7, 447.4 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "HTC/*/HTC One/*" },
|
|
{ "ua": "HTC One" }
|
|
],
|
|
"dpi": 472.8,
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "Huawei/*/Nexus 6P/*" },
|
|
{ "ua": "Nexus 6P" }
|
|
],
|
|
"dpi": [ 515.1, 518.0 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "LGE/*/Nexus 5X/*" },
|
|
{ "ua": "Nexus 5X" }
|
|
],
|
|
"dpi": [ 422.0, 419.9 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "LGE/*/LGMS345/*" },
|
|
{ "ua": "LGMS345" }
|
|
],
|
|
"dpi": [ 221.7, 219.1 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "LGE/*/LG-D800/*" },
|
|
{ "ua": "LG-D800" }
|
|
],
|
|
"dpi": [ 422.0, 424.1 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "LGE/*/LG-D850/*" },
|
|
{ "ua": "LG-D850" }
|
|
],
|
|
"dpi": [ 537.9, 541.9 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "LGE/*/VS985 4G/*" },
|
|
{ "ua": "VS985 4G" }
|
|
],
|
|
"dpi": [ 537.9, 535.6 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "LGE/*/Nexus 5/*" },
|
|
{ "ua": "Nexus 5 " }
|
|
],
|
|
"dpi": [ 442.4, 444.8 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "LGE/*/Nexus 4/*" },
|
|
{ "ua": "Nexus 4" }
|
|
],
|
|
"dpi": [ 319.8, 318.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "LGE/*/LG-P769/*" },
|
|
{ "ua": "LG-P769" }
|
|
],
|
|
"dpi": [ 240.6, 247.5 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "LGE/*/LGMS323/*" },
|
|
{ "ua": "LGMS323" }
|
|
],
|
|
"dpi": [ 206.6, 204.6 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "LGE/*/LGLS996/*" },
|
|
{ "ua": "LGLS996" }
|
|
],
|
|
"dpi": [ 403.4, 401.5 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "Micromax/*/4560MMX/*" },
|
|
{ "ua": "4560MMX" }
|
|
],
|
|
"dpi": [ 240.0, 219.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "Micromax/*/A250/*" },
|
|
{ "ua": "Micromax A250" }
|
|
],
|
|
"dpi": [ 480.0, 446.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "Micromax/*/Micromax AQ4501/*" },
|
|
{ "ua": "Micromax AQ4501" }
|
|
],
|
|
"dpi": 240.0,
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/DROID RAZR/*" },
|
|
{ "ua": "DROID RAZR" }
|
|
],
|
|
"dpi": [ 368.1, 256.7 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/XT830C/*" },
|
|
{ "ua": "XT830C" }
|
|
],
|
|
"dpi": [ 254.0, 255.9 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/XT1021/*" },
|
|
{ "ua": "XT1021" }
|
|
],
|
|
"dpi": [ 254.0, 256.7 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/XT1023/*" },
|
|
{ "ua": "XT1023" }
|
|
],
|
|
"dpi": [ 254.0, 256.7 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/XT1028/*" },
|
|
{ "ua": "XT1028" }
|
|
],
|
|
"dpi": [ 326.6, 327.6 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/XT1034/*" },
|
|
{ "ua": "XT1034" }
|
|
],
|
|
"dpi": [ 326.6, 328.4 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/XT1053/*" },
|
|
{ "ua": "XT1053" }
|
|
],
|
|
"dpi": [ 315.3, 316.1 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/XT1562/*" },
|
|
{ "ua": "XT1562" }
|
|
],
|
|
"dpi": [ 403.4, 402.7 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/Nexus 6/*" },
|
|
{ "ua": "Nexus 6 " }
|
|
],
|
|
"dpi": [ 494.3, 489.7 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/XT1063/*" },
|
|
{ "ua": "XT1063" }
|
|
],
|
|
"dpi": [ 295.0, 296.6 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/XT1064/*" },
|
|
{ "ua": "XT1064" }
|
|
],
|
|
"dpi": [ 295.0, 295.6 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/XT1092/*" },
|
|
{ "ua": "XT1092" }
|
|
],
|
|
"dpi": [ 422.0, 424.1 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "motorola/*/XT1095/*" },
|
|
{ "ua": "XT1095" }
|
|
],
|
|
"dpi": [ 422.0, 423.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "OnePlus/*/A0001/*" },
|
|
{ "ua": "A0001" }
|
|
],
|
|
"dpi": [ 403.4, 401.0 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "OnePlus/*/ONE E1005/*" },
|
|
{ "ua": "ONE E1005" }
|
|
],
|
|
"dpi": [ 442.4, 441.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "OnePlus/*/ONE A2005/*" },
|
|
{ "ua": "ONE A2005" }
|
|
],
|
|
"dpi": [ 391.9, 405.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "OPPO/*/X909/*" },
|
|
{ "ua": "X909" }
|
|
],
|
|
"dpi": [ 442.4, 444.1 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/GT-I9082/*" },
|
|
{ "ua": "GT-I9082" }
|
|
],
|
|
"dpi": [ 184.7, 185.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-G360P/*" },
|
|
{ "ua": "SM-G360P" }
|
|
],
|
|
"dpi": [ 196.7, 205.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/Nexus S/*" },
|
|
{ "ua": "Nexus S" }
|
|
],
|
|
"dpi": [ 234.5, 229.8 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/GT-I9300/*" },
|
|
{ "ua": "GT-I9300" }
|
|
],
|
|
"dpi": [ 304.8, 303.9 ],
|
|
"bw": 5,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-T230NU/*" },
|
|
{ "ua": "SM-T230NU" }
|
|
],
|
|
"dpi": 216.0,
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SGH-T399/*" },
|
|
{ "ua": "SGH-T399" }
|
|
],
|
|
"dpi": [ 217.7, 231.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-N9005/*" },
|
|
{ "ua": "SM-N9005" }
|
|
],
|
|
"dpi": [ 386.4, 387.0 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SAMSUNG-SM-N900A/*" },
|
|
{ "ua": "SAMSUNG-SM-N900A" }
|
|
],
|
|
"dpi": [ 386.4, 387.7 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/GT-I9500/*" },
|
|
{ "ua": "GT-I9500" }
|
|
],
|
|
"dpi": [ 442.5, 443.3 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/GT-I9505/*" },
|
|
{ "ua": "GT-I9505" }
|
|
],
|
|
"dpi": 439.4,
|
|
"bw": 4,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-G900F/*" },
|
|
{ "ua": "SM-G900F" }
|
|
],
|
|
"dpi": [ 415.6, 431.6 ],
|
|
"bw": 5,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-G900M/*" },
|
|
{ "ua": "SM-G900M" }
|
|
],
|
|
"dpi": [ 415.6, 431.6 ],
|
|
"bw": 5,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-G800F/*" },
|
|
{ "ua": "SM-G800F" }
|
|
],
|
|
"dpi": 326.8,
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-G906S/*" },
|
|
{ "ua": "SM-G906S" }
|
|
],
|
|
"dpi": [ 562.7, 572.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/GT-I9300/*" },
|
|
{ "ua": "GT-I9300" }
|
|
],
|
|
"dpi": [ 306.7, 304.8 ],
|
|
"bw": 5,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-T535/*" },
|
|
{ "ua": "SM-T535" }
|
|
],
|
|
"dpi": [ 142.6, 136.4 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-N920C/*" },
|
|
{ "ua": "SM-N920C" }
|
|
],
|
|
"dpi": [ 515.1, 518.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/GT-I9300I/*" },
|
|
{ "ua": "GT-I9300I" }
|
|
],
|
|
"dpi": [ 304.8, 305.8 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/GT-I9195/*" },
|
|
{ "ua": "GT-I9195" }
|
|
],
|
|
"dpi": [ 249.4, 256.7 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SPH-L520/*" },
|
|
{ "ua": "SPH-L520" }
|
|
],
|
|
"dpi": [ 249.4, 255.9 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SAMSUNG-SGH-I717/*" },
|
|
{ "ua": "SAMSUNG-SGH-I717" }
|
|
],
|
|
"dpi": 285.8,
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SPH-D710/*" },
|
|
{ "ua": "SPH-D710" }
|
|
],
|
|
"dpi": [ 217.7, 204.2 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/GT-N7100/*" },
|
|
{ "ua": "GT-N7100" }
|
|
],
|
|
"dpi": 265.1,
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SCH-I605/*" },
|
|
{ "ua": "SCH-I605" }
|
|
],
|
|
"dpi": 265.1,
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/Galaxy Nexus/*" },
|
|
{ "ua": "Galaxy Nexus" }
|
|
],
|
|
"dpi": [ 315.3, 314.2 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-N910H/*" },
|
|
{ "ua": "SM-N910H" }
|
|
],
|
|
"dpi": [ 515.1, 518.0 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-N910C/*" },
|
|
{ "ua": "SM-N910C" }
|
|
],
|
|
"dpi": [ 515.2, 520.2 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-G130M/*" },
|
|
{ "ua": "SM-G130M" }
|
|
],
|
|
"dpi": [ 165.9, 164.8 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-G928I/*" },
|
|
{ "ua": "SM-G928I" }
|
|
],
|
|
"dpi": [ 515.1, 518.4 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-G920F/*" },
|
|
{ "ua": "SM-G920F" }
|
|
],
|
|
"dpi": 580.6,
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-G920P/*" },
|
|
{ "ua": "SM-G920P" }
|
|
],
|
|
"dpi": [ 522.5, 577.0 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-G925F/*" },
|
|
{ "ua": "SM-G925F" }
|
|
],
|
|
"dpi": 580.6,
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "samsung/*/SM-G925V/*" },
|
|
{ "ua": "SM-G925V" }
|
|
],
|
|
"dpi": [ 522.5, 576.6 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "Sony/*/C6903/*" },
|
|
{ "ua": "C6903" }
|
|
],
|
|
"dpi": [ 442.5, 443.3 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "Sony/*/D6653/*" },
|
|
{ "ua": "D6653" }
|
|
],
|
|
"dpi": [ 428.6, 427.6 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "Sony/*/E6653/*" },
|
|
{ "ua": "E6653" }
|
|
],
|
|
"dpi": [ 428.6, 425.7 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "Sony/*/E6853/*" },
|
|
{ "ua": "E6853" }
|
|
],
|
|
"dpi": [ 403.4, 401.9 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "Sony/*/SGP321/*" },
|
|
{ "ua": "SGP321" }
|
|
],
|
|
"dpi": [ 224.7, 224.1 ],
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "TCT/*/ALCATEL ONE TOUCH Fierce/*" },
|
|
{ "ua": "ALCATEL ONE TOUCH Fierce" }
|
|
],
|
|
"dpi": [ 240.0, 247.5 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "THL/*/thl 5000/*" },
|
|
{ "ua": "thl 5000" }
|
|
],
|
|
"dpi": [ 480.0, 443.3 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "android",
|
|
"rules": [
|
|
{ "mdmh": "ZTE/*/ZTE Blade L2/*" },
|
|
{ "ua": "ZTE Blade L2" }
|
|
],
|
|
"dpi": 240.0,
|
|
"bw": 3,
|
|
"ac": 500
|
|
},
|
|
|
|
{
|
|
"type": "ios",
|
|
"rules": [ { "res": [ 640, 960 ] } ],
|
|
"dpi": [ 325.1, 328.4 ],
|
|
"bw": 4,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "ios",
|
|
"rules": [ { "res": [ 640, 960 ] } ],
|
|
"dpi": [ 325.1, 328.4 ],
|
|
"bw": 4,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "ios",
|
|
"rules": [ { "res": [ 640, 1136 ] } ],
|
|
"dpi": [ 317.1, 320.2 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "ios",
|
|
"rules": [ { "res": [ 640, 1136 ] } ],
|
|
"dpi": [ 317.1, 320.2 ],
|
|
"bw": 3,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "ios",
|
|
"rules": [ { "res": [ 750, 1334 ] } ],
|
|
"dpi": 326.4,
|
|
"bw": 4,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "ios",
|
|
"rules": [ { "res": [ 750, 1334 ] } ],
|
|
"dpi": 326.4,
|
|
"bw": 4,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "ios",
|
|
"rules": [ { "res": [ 1242, 2208 ] } ],
|
|
"dpi": [ 453.6, 458.4 ],
|
|
"bw": 4,
|
|
"ac": 1000
|
|
},
|
|
|
|
{
|
|
"type": "ios",
|
|
"rules": [ { "res": [ 1242, 2208 ] } ],
|
|
"dpi": [ 453.6, 458.4 ],
|
|
"bw": 4,
|
|
"ac": 1000
|
|
}
|
|
]};
|
|
|
|
module.exports = DPDB_CACHE;
|
|
|
|
},{}],11:[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.
|
|
*/
|
|
|
|
// Offline cache of the DPDB, to be used until we load the online one (and
|
|
// as a fallback in case we can't load the online one).
|
|
var DPDB_CACHE = _dereq_('./dpdb-cache.js');
|
|
var Util = _dereq_('../util.js');
|
|
|
|
// Online DPDB URL.
|
|
var ONLINE_DPDB_URL = 'https://storage.googleapis.com/cardboard-dpdb/dpdb.json';
|
|
|
|
/**
|
|
* Calculates device parameters based on the DPDB (Device Parameter Database).
|
|
* Initially, uses the cached DPDB values.
|
|
*
|
|
* If fetchOnline == true, then this object tries to fetch the online version
|
|
* of the DPDB and updates the device info if a better match is found.
|
|
* Calls the onDeviceParamsUpdated callback when there is an update to the
|
|
* device information.
|
|
*/
|
|
function Dpdb(fetchOnline, onDeviceParamsUpdated) {
|
|
// Start with the offline DPDB cache while we are loading the real one.
|
|
this.dpdb = DPDB_CACHE;
|
|
|
|
// Calculate device params based on the offline version of the DPDB.
|
|
this.recalculateDeviceParams_();
|
|
|
|
// XHR to fetch online DPDB file, if requested.
|
|
if (fetchOnline) {
|
|
// Set the callback.
|
|
this.onDeviceParamsUpdated = onDeviceParamsUpdated;
|
|
|
|
console.log('Fetching DPDB...');
|
|
var xhr = new XMLHttpRequest();
|
|
var obj = this;
|
|
xhr.open('GET', ONLINE_DPDB_URL, true);
|
|
xhr.addEventListener('load', function() {
|
|
obj.loading = false;
|
|
if (xhr.status >= 200 && xhr.status <= 299) {
|
|
// Success.
|
|
console.log('Successfully loaded online DPDB.');
|
|
obj.dpdb = JSON.parse(xhr.response);
|
|
obj.recalculateDeviceParams_();
|
|
} else {
|
|
// Error loading the DPDB.
|
|
console.error('Error loading online DPDB!');
|
|
}
|
|
});
|
|
xhr.send();
|
|
}
|
|
}
|
|
|
|
// Returns the current device parameters.
|
|
Dpdb.prototype.getDeviceParams = function() {
|
|
return this.deviceParams;
|
|
};
|
|
|
|
// Recalculates this device's parameters based on the DPDB.
|
|
Dpdb.prototype.recalculateDeviceParams_ = function() {
|
|
console.log('Recalculating device params.');
|
|
var newDeviceParams = this.calcDeviceParams_();
|
|
console.log('New device parameters:');
|
|
console.log(newDeviceParams);
|
|
if (newDeviceParams) {
|
|
this.deviceParams = newDeviceParams;
|
|
// Invoke callback, if it is set.
|
|
if (this.onDeviceParamsUpdated) {
|
|
this.onDeviceParamsUpdated(this.deviceParams);
|
|
}
|
|
} else {
|
|
console.error('Failed to recalculate device parameters.');
|
|
}
|
|
};
|
|
|
|
// Returns a DeviceParams object that represents the best guess as to this
|
|
// device's parameters. Can return null if the device does not match any
|
|
// known devices.
|
|
Dpdb.prototype.calcDeviceParams_ = function() {
|
|
var db = this.dpdb; // shorthand
|
|
if (!db) {
|
|
console.error('DPDB not available.');
|
|
return null;
|
|
}
|
|
if (db.format != 1) {
|
|
console.error('DPDB has unexpected format version.');
|
|
return null;
|
|
}
|
|
if (!db.devices || !db.devices.length) {
|
|
console.error('DPDB does not have a devices section.');
|
|
return null;
|
|
}
|
|
|
|
// Get the actual user agent and screen dimensions in pixels.
|
|
var userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
|
var width = Util.getScreenWidth();
|
|
var height = Util.getScreenHeight();
|
|
console.log('User agent: ' + userAgent);
|
|
console.log('Pixel width: ' + width);
|
|
console.log('Pixel height: ' + height);
|
|
|
|
if (!db.devices) {
|
|
console.error('DPDB has no devices section.');
|
|
return null;
|
|
}
|
|
|
|
for (var i = 0; i < db.devices.length; i++) {
|
|
var device = db.devices[i];
|
|
if (!device.rules) {
|
|
console.warn('Device[' + i + '] has no rules section.');
|
|
continue;
|
|
}
|
|
|
|
if (device.type != 'ios' && device.type != 'android') {
|
|
console.warn('Device[' + i + '] has invalid type.');
|
|
continue;
|
|
}
|
|
|
|
// See if this device is of the appropriate type.
|
|
if (Util.isIOS() != (device.type == 'ios')) continue;
|
|
|
|
// See if this device matches any of the rules:
|
|
var matched = false;
|
|
for (var j = 0; j < device.rules.length; j++) {
|
|
var rule = device.rules[j];
|
|
if (this.matchRule_(rule, userAgent, width, height)) {
|
|
console.log('Rule matched:');
|
|
console.log(rule);
|
|
matched = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!matched) continue;
|
|
|
|
// device.dpi might be an array of [ xdpi, ydpi] or just a scalar.
|
|
var xdpi = device.dpi[0] || device.dpi;
|
|
var ydpi = device.dpi[1] || device.dpi;
|
|
|
|
return new DeviceParams({ xdpi: xdpi, ydpi: ydpi, bevelMm: device.bw });
|
|
}
|
|
|
|
console.warn('No DPDB device match.');
|
|
return null;
|
|
};
|
|
|
|
Dpdb.prototype.matchRule_ = function(rule, ua, screenWidth, screenHeight) {
|
|
// We can only match 'ua' and 'res' rules, not other types like 'mdmh'
|
|
// (which are meant for native platforms).
|
|
if (!rule.ua && !rule.res) return false;
|
|
|
|
// If our user agent string doesn't contain the indicated user agent string,
|
|
// the match fails.
|
|
if (rule.ua && ua.indexOf(rule.ua) < 0) return false;
|
|
|
|
// If the rule specifies screen dimensions that don't correspond to ours,
|
|
// the match fails.
|
|
if (rule.res) {
|
|
if (!rule.res[0] || !rule.res[1]) return false;
|
|
var resX = rule.res[0];
|
|
var resY = rule.res[1];
|
|
// Compare min and max so as to make the order not matter, i.e., it should
|
|
// be true that 640x480 == 480x640.
|
|
if (Math.min(screenWidth, screenHeight) != Math.min(resX, resY) ||
|
|
(Math.max(screenWidth, screenHeight) != Math.max(resX, resY))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function DeviceParams(params) {
|
|
this.xdpi = params.xdpi;
|
|
this.ydpi = params.ydpi;
|
|
this.bevelMm = params.bevelMm;
|
|
}
|
|
|
|
module.exports = Dpdb;
|
|
},{"../util.js":22,"./dpdb-cache.js":10}],12:[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.
|
|
*/
|
|
|
|
function Emitter() {
|
|
this.callbacks = {};
|
|
}
|
|
|
|
Emitter.prototype.emit = function(eventName) {
|
|
var callbacks = this.callbacks[eventName];
|
|
if (!callbacks) {
|
|
//console.log('No valid callback specified.');
|
|
return;
|
|
}
|
|
var args = [].slice.call(arguments);
|
|
// Eliminate the first param (the callback).
|
|
args.shift();
|
|
for (var i = 0; i < callbacks.length; i++) {
|
|
callbacks[i].apply(this, args);
|
|
}
|
|
};
|
|
|
|
Emitter.prototype.on = function(eventName, callback) {
|
|
if (eventName in this.callbacks) {
|
|
this.callbacks[eventName].push(callback);
|
|
} else {
|
|
this.callbacks[eventName] = [callback];
|
|
}
|
|
};
|
|
|
|
module.exports = Emitter;
|
|
|
|
},{}],13:[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 WebVRPolyfill = _dereq_('./webvr-polyfill.js').WebVRPolyfill;
|
|
var InstallWebVRSpecShim = _dereq_('./webvr-polyfill.js').InstallWebVRSpecShim;
|
|
|
|
// Initialize a WebVRConfig just in case.
|
|
window.WebVRConfig = Util.extend({
|
|
// Forces availability of VR mode, even for non-mobile devices.
|
|
FORCE_ENABLE_VR: false,
|
|
|
|
// Complementary filter coefficient. 0 for accelerometer, 1 for gyro.
|
|
K_FILTER: 0.98,
|
|
|
|
// How far into the future to predict during fast motion (in seconds).
|
|
PREDICTION_TIME_S: 0.040,
|
|
|
|
// Flag to disable touch panner. In case you have your own touch controls.
|
|
TOUCH_PANNER_DISABLED: false,
|
|
|
|
// Flag to disabled the UI in VR Mode.
|
|
CARDBOARD_UI_DISABLED: false, // Default: false
|
|
|
|
// Flag to disable the instructions to rotate your device.
|
|
ROTATE_INSTRUCTIONS_DISABLED: false, // Default: false.
|
|
|
|
// Enable yaw panning only, disabling roll and pitch. This can be useful
|
|
// for panoramas with nothing interesting above or below.
|
|
YAW_ONLY: false,
|
|
|
|
// To disable keyboard and mouse controls, if you want to use your own
|
|
// implementation.
|
|
MOUSE_KEYBOARD_CONTROLS_DISABLED: false,
|
|
|
|
// Prevent the polyfill from initializing immediately. Requires the app
|
|
// to call InitializeWebVRPolyfill() before it can be used.
|
|
DEFER_INITIALIZATION: false,
|
|
|
|
// Enable the deprecated version of the API (navigator.getVRDevices).
|
|
ENABLE_DEPRECATED_API: false,
|
|
|
|
// Scales the recommended buffer size reported by WebVR, which can improve
|
|
// performance.
|
|
// UPDATE(2016-05-03): Setting this to 0.5 by default since 1.0 does not
|
|
// perform well on many mobile devices.
|
|
BUFFER_SCALE: 0.5,
|
|
|
|
// Allow VRDisplay.submitFrame to change gl bindings, which is more
|
|
// efficient if the application code will re-bind its resources on the
|
|
// next frame anyway. This has been seen to cause rendering glitches with
|
|
// THREE.js.
|
|
// Dirty bindings include: gl.FRAMEBUFFER_BINDING, gl.CURRENT_PROGRAM,
|
|
// gl.ARRAY_BUFFER_BINDING, gl.ELEMENT_ARRAY_BUFFER_BINDING,
|
|
// and gl.TEXTURE_BINDING_2D for texture unit 0.
|
|
DIRTY_SUBMIT_FRAME_BINDINGS: false
|
|
}, window.WebVRConfig);
|
|
|
|
if (!window.WebVRConfig.DEFER_INITIALIZATION) {
|
|
new WebVRPolyfill();
|
|
} else {
|
|
window.InitializeWebVRPolyfill = function() {
|
|
new WebVRPolyfill();
|
|
}
|
|
// Call this if you want to use the shim without the rest of the polyfill.
|
|
// InitializeWebVRPolyfill() will install the shim automatically when needed,
|
|
// so this should rarely be used.
|
|
window.InitializeSpecShim = function() {
|
|
InstallWebVRSpecShim();
|
|
}
|
|
}
|
|
|
|
},{"./util.js":22,"./webvr-polyfill.js":25}],14:[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 MathUtil = window.MathUtil || {};
|
|
|
|
MathUtil.degToRad = Math.PI / 180;
|
|
MathUtil.radToDeg = 180 / Math.PI;
|
|
|
|
// Some minimal math functionality borrowed from THREE.Math and stripped down
|
|
// for the purposes of this library.
|
|
|
|
|
|
MathUtil.Vector2 = function ( x, y ) {
|
|
this.x = x || 0;
|
|
this.y = y || 0;
|
|
};
|
|
|
|
MathUtil.Vector2.prototype = {
|
|
constructor: MathUtil.Vector2,
|
|
|
|
set: function ( x, y ) {
|
|
this.x = x;
|
|
this.y = y;
|
|
|
|
return this;
|
|
},
|
|
|
|
copy: function ( v ) {
|
|
this.x = v.x;
|
|
this.y = v.y;
|
|
|
|
return this;
|
|
},
|
|
|
|
subVectors: function ( a, b ) {
|
|
this.x = a.x - b.x;
|
|
this.y = a.y - b.y;
|
|
|
|
return this;
|
|
},
|
|
};
|
|
|
|
MathUtil.Vector3 = function ( x, y, z ) {
|
|
this.x = x || 0;
|
|
this.y = y || 0;
|
|
this.z = z || 0;
|
|
};
|
|
|
|
MathUtil.Vector3.prototype = {
|
|
constructor: MathUtil.Vector3,
|
|
|
|
set: function ( x, y, z ) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
|
|
return this;
|
|
},
|
|
|
|
copy: function ( v ) {
|
|
this.x = v.x;
|
|
this.y = v.y;
|
|
this.z = v.z;
|
|
|
|
return this;
|
|
},
|
|
|
|
length: function () {
|
|
return Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z );
|
|
},
|
|
|
|
normalize: function () {
|
|
var scalar = this.length();
|
|
|
|
if ( scalar !== 0 ) {
|
|
var invScalar = 1 / scalar;
|
|
|
|
this.multiplyScalar(invScalar);
|
|
} else {
|
|
this.x = 0;
|
|
this.y = 0;
|
|
this.z = 0;
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
multiplyScalar: function ( scalar ) {
|
|
this.x *= scalar;
|
|
this.y *= scalar;
|
|
this.z *= scalar;
|
|
},
|
|
|
|
applyQuaternion: function ( q ) {
|
|
var x = this.x;
|
|
var y = this.y;
|
|
var z = this.z;
|
|
|
|
var qx = q.x;
|
|
var qy = q.y;
|
|
var qz = q.z;
|
|
var qw = q.w;
|
|
|
|
// calculate quat * vector
|
|
var ix = qw * x + qy * z - qz * y;
|
|
var iy = qw * y + qz * x - qx * z;
|
|
var iz = qw * z + qx * y - qy * x;
|
|
var iw = - qx * x - qy * y - qz * z;
|
|
|
|
// calculate result * inverse quat
|
|
this.x = ix * qw + iw * - qx + iy * - qz - iz * - qy;
|
|
this.y = iy * qw + iw * - qy + iz * - qx - ix * - qz;
|
|
this.z = iz * qw + iw * - qz + ix * - qy - iy * - qx;
|
|
|
|
return this;
|
|
},
|
|
|
|
dot: function ( v ) {
|
|
return this.x * v.x + this.y * v.y + this.z * v.z;
|
|
},
|
|
|
|
crossVectors: function ( a, b ) {
|
|
var ax = a.x, ay = a.y, az = a.z;
|
|
var bx = b.x, by = b.y, bz = b.z;
|
|
|
|
this.x = ay * bz - az * by;
|
|
this.y = az * bx - ax * bz;
|
|
this.z = ax * by - ay * bx;
|
|
|
|
return this;
|
|
},
|
|
};
|
|
|
|
MathUtil.Quaternion = function ( x, y, z, w ) {
|
|
this.x = x || 0;
|
|
this.y = y || 0;
|
|
this.z = z || 0;
|
|
this.w = ( w !== undefined ) ? w : 1;
|
|
};
|
|
|
|
MathUtil.Quaternion.prototype = {
|
|
constructor: MathUtil.Quaternion,
|
|
|
|
set: function ( x, y, z, w ) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.w = w;
|
|
|
|
return this;
|
|
},
|
|
|
|
copy: function ( quaternion ) {
|
|
this.x = quaternion.x;
|
|
this.y = quaternion.y;
|
|
this.z = quaternion.z;
|
|
this.w = quaternion.w;
|
|
|
|
return this;
|
|
},
|
|
|
|
setFromEulerXYZ: function( x, y, z ) {
|
|
var c1 = Math.cos( x / 2 );
|
|
var c2 = Math.cos( y / 2 );
|
|
var c3 = Math.cos( z / 2 );
|
|
var s1 = Math.sin( x / 2 );
|
|
var s2 = Math.sin( y / 2 );
|
|
var s3 = Math.sin( z / 2 );
|
|
|
|
this.x = s1 * c2 * c3 + c1 * s2 * s3;
|
|
this.y = c1 * s2 * c3 - s1 * c2 * s3;
|
|
this.z = c1 * c2 * s3 + s1 * s2 * c3;
|
|
this.w = c1 * c2 * c3 - s1 * s2 * s3;
|
|
|
|
return this;
|
|
},
|
|
|
|
setFromEulerYXZ: function( x, y, z ) {
|
|
var c1 = Math.cos( x / 2 );
|
|
var c2 = Math.cos( y / 2 );
|
|
var c3 = Math.cos( z / 2 );
|
|
var s1 = Math.sin( x / 2 );
|
|
var s2 = Math.sin( y / 2 );
|
|
var s3 = Math.sin( z / 2 );
|
|
|
|
this.x = s1 * c2 * c3 + c1 * s2 * s3;
|
|
this.y = c1 * s2 * c3 - s1 * c2 * s3;
|
|
this.z = c1 * c2 * s3 - s1 * s2 * c3;
|
|
this.w = c1 * c2 * c3 + s1 * s2 * s3;
|
|
|
|
return this;
|
|
},
|
|
|
|
setFromAxisAngle: function ( axis, angle ) {
|
|
// http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm
|
|
// assumes axis is normalized
|
|
|
|
var halfAngle = angle / 2, s = Math.sin( halfAngle );
|
|
|
|
this.x = axis.x * s;
|
|
this.y = axis.y * s;
|
|
this.z = axis.z * s;
|
|
this.w = Math.cos( halfAngle );
|
|
|
|
return this;
|
|
},
|
|
|
|
multiply: function ( q ) {
|
|
return this.multiplyQuaternions( this, q );
|
|
},
|
|
|
|
multiplyQuaternions: function ( a, b ) {
|
|
// from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm
|
|
|
|
var qax = a.x, qay = a.y, qaz = a.z, qaw = a.w;
|
|
var qbx = b.x, qby = b.y, qbz = b.z, qbw = b.w;
|
|
|
|
this.x = qax * qbw + qaw * qbx + qay * qbz - qaz * qby;
|
|
this.y = qay * qbw + qaw * qby + qaz * qbx - qax * qbz;
|
|
this.z = qaz * qbw + qaw * qbz + qax * qby - qay * qbx;
|
|
this.w = qaw * qbw - qax * qbx - qay * qby - qaz * qbz;
|
|
|
|
return this;
|
|
},
|
|
|
|
inverse: function () {
|
|
this.x *= -1;
|
|
this.y *= -1;
|
|
this.z *= -1;
|
|
|
|
this.normalize();
|
|
|
|
return this;
|
|
},
|
|
|
|
normalize: function () {
|
|
var l = Math.sqrt( this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w );
|
|
|
|
if ( l === 0 ) {
|
|
this.x = 0;
|
|
this.y = 0;
|
|
this.z = 0;
|
|
this.w = 1;
|
|
} else {
|
|
l = 1 / l;
|
|
|
|
this.x = this.x * l;
|
|
this.y = this.y * l;
|
|
this.z = this.z * l;
|
|
this.w = this.w * l;
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
slerp: function ( qb, t ) {
|
|
if ( t === 0 ) return this;
|
|
if ( t === 1 ) return this.copy( qb );
|
|
|
|
var x = this.x, y = this.y, z = this.z, w = this.w;
|
|
|
|
// http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/
|
|
|
|
var cosHalfTheta = w * qb.w + x * qb.x + y * qb.y + z * qb.z;
|
|
|
|
if ( cosHalfTheta < 0 ) {
|
|
this.w = - qb.w;
|
|
this.x = - qb.x;
|
|
this.y = - qb.y;
|
|
this.z = - qb.z;
|
|
|
|
cosHalfTheta = - cosHalfTheta;
|
|
} else {
|
|
this.copy( qb );
|
|
}
|
|
|
|
if ( cosHalfTheta >= 1.0 ) {
|
|
this.w = w;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
|
|
return this;
|
|
}
|
|
|
|
var halfTheta = Math.acos( cosHalfTheta );
|
|
var sinHalfTheta = Math.sqrt( 1.0 - cosHalfTheta * cosHalfTheta );
|
|
|
|
if ( Math.abs( sinHalfTheta ) < 0.001 ) {
|
|
this.w = 0.5 * ( w + this.w );
|
|
this.x = 0.5 * ( x + this.x );
|
|
this.y = 0.5 * ( y + this.y );
|
|
this.z = 0.5 * ( z + this.z );
|
|
|
|
return this;
|
|
}
|
|
|
|
var ratioA = Math.sin( ( 1 - t ) * halfTheta ) / sinHalfTheta,
|
|
ratioB = Math.sin( t * halfTheta ) / sinHalfTheta;
|
|
|
|
this.w = ( w * ratioA + this.w * ratioB );
|
|
this.x = ( x * ratioA + this.x * ratioB );
|
|
this.y = ( y * ratioA + this.y * ratioB );
|
|
this.z = ( z * ratioA + this.z * ratioB );
|
|
|
|
return this;
|
|
},
|
|
|
|
setFromUnitVectors: function () {
|
|
// http://lolengine.net/blog/2014/02/24/quaternion-from-two-vectors-final
|
|
// assumes direction vectors vFrom and vTo are normalized
|
|
|
|
var v1, r;
|
|
var EPS = 0.000001;
|
|
|
|
return function ( vFrom, vTo ) {
|
|
if ( v1 === undefined ) v1 = new MathUtil.Vector3();
|
|
|
|
r = vFrom.dot( vTo ) + 1;
|
|
|
|
if ( r < EPS ) {
|
|
r = 0;
|
|
|
|
if ( Math.abs( vFrom.x ) > Math.abs( vFrom.z ) ) {
|
|
v1.set( - vFrom.y, vFrom.x, 0 );
|
|
} else {
|
|
v1.set( 0, - vFrom.z, vFrom.y );
|
|
}
|
|
} else {
|
|
v1.crossVectors( vFrom, vTo );
|
|
}
|
|
|
|
this.x = v1.x;
|
|
this.y = v1.y;
|
|
this.z = v1.z;
|
|
this.w = r;
|
|
|
|
this.normalize();
|
|
|
|
return this;
|
|
}
|
|
}(),
|
|
};
|
|
|
|
module.exports = MathUtil;
|
|
|
|
},{}],15:[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 VRDisplay = _dereq_('./base.js').VRDisplay;
|
|
var MathUtil = _dereq_('./math-util.js');
|
|
var Util = _dereq_('./util.js');
|
|
|
|
// How much to rotate per key stroke.
|
|
var KEY_SPEED = 0.15;
|
|
var KEY_ANIMATION_DURATION = 80;
|
|
|
|
// How much to rotate for mouse events.
|
|
var MOUSE_SPEED_X = 0.5;
|
|
var MOUSE_SPEED_Y = 0.3;
|
|
|
|
/**
|
|
* VRDisplay based on mouse and keyboard input. Designed for desktops/laptops
|
|
* where orientation events aren't supported. Cannot present.
|
|
*/
|
|
function MouseKeyboardVRDisplay() {
|
|
this.displayName = 'Mouse and Keyboard VRDisplay (webvr-polyfill)';
|
|
|
|
this.capabilities.hasOrientation = true;
|
|
|
|
// Attach to mouse and keyboard events.
|
|
window.addEventListener('keydown', this.onKeyDown_.bind(this));
|
|
window.addEventListener('mousemove', this.onMouseMove_.bind(this));
|
|
window.addEventListener('mousedown', this.onMouseDown_.bind(this));
|
|
window.addEventListener('mouseup', this.onMouseUp_.bind(this));
|
|
|
|
// "Private" members.
|
|
this.phi_ = 0;
|
|
this.theta_ = 0;
|
|
|
|
// Variables for keyboard-based rotation animation.
|
|
this.targetAngle_ = null;
|
|
this.angleAnimation_ = null;
|
|
|
|
// State variables for calculations.
|
|
this.orientation_ = new MathUtil.Quaternion();
|
|
|
|
// Variables for mouse-based rotation.
|
|
this.rotateStart_ = new MathUtil.Vector2();
|
|
this.rotateEnd_ = new MathUtil.Vector2();
|
|
this.rotateDelta_ = new MathUtil.Vector2();
|
|
this.isDragging_ = false;
|
|
|
|
this.orientationOut_ = new Float32Array(4);
|
|
}
|
|
MouseKeyboardVRDisplay.prototype = new VRDisplay();
|
|
|
|
MouseKeyboardVRDisplay.prototype.getImmediatePose = function() {
|
|
this.orientation_.setFromEulerYXZ(this.phi_, this.theta_, 0);
|
|
|
|
this.orientationOut_[0] = this.orientation_.x;
|
|
this.orientationOut_[1] = this.orientation_.y;
|
|
this.orientationOut_[2] = this.orientation_.z;
|
|
this.orientationOut_[3] = this.orientation_.w;
|
|
|
|
return {
|
|
position: null,
|
|
orientation: this.orientationOut_,
|
|
linearVelocity: null,
|
|
linearAcceleration: null,
|
|
angularVelocity: null,
|
|
angularAcceleration: null
|
|
};
|
|
};
|
|
|
|
MouseKeyboardVRDisplay.prototype.onKeyDown_ = function(e) {
|
|
// Track WASD and arrow keys.
|
|
if (e.keyCode == 38) { // Up key.
|
|
this.animatePhi_(this.phi_ + KEY_SPEED);
|
|
} else if (e.keyCode == 39) { // Right key.
|
|
this.animateTheta_(this.theta_ - KEY_SPEED);
|
|
} else if (e.keyCode == 40) { // Down key.
|
|
this.animatePhi_(this.phi_ - KEY_SPEED);
|
|
} else if (e.keyCode == 37) { // Left key.
|
|
this.animateTheta_(this.theta_ + KEY_SPEED);
|
|
}
|
|
};
|
|
|
|
MouseKeyboardVRDisplay.prototype.animateTheta_ = function(targetAngle) {
|
|
this.animateKeyTransitions_('theta_', targetAngle);
|
|
};
|
|
|
|
MouseKeyboardVRDisplay.prototype.animatePhi_ = function(targetAngle) {
|
|
// Prevent looking too far up or down.
|
|
targetAngle = Util.clamp(targetAngle, -Math.PI/2, Math.PI/2);
|
|
this.animateKeyTransitions_('phi_', targetAngle);
|
|
};
|
|
|
|
/**
|
|
* Start an animation to transition an angle from one value to another.
|
|
*/
|
|
MouseKeyboardVRDisplay.prototype.animateKeyTransitions_ = function(angleName, targetAngle) {
|
|
// If an animation is currently running, cancel it.
|
|
if (this.angleAnimation_) {
|
|
cancelAnimationFrame(this.angleAnimation_);
|
|
}
|
|
var startAngle = this[angleName];
|
|
var startTime = new Date();
|
|
// Set up an interval timer to perform the animation.
|
|
this.angleAnimation_ = requestAnimationFrame(function animate() {
|
|
// Once we're finished the animation, we're done.
|
|
var elapsed = new Date() - startTime;
|
|
if (elapsed >= KEY_ANIMATION_DURATION) {
|
|
this[angleName] = targetAngle;
|
|
cancelAnimationFrame(this.angleAnimation_);
|
|
return;
|
|
}
|
|
// loop with requestAnimationFrame
|
|
this.angleAnimation_ = requestAnimationFrame(animate.bind(this))
|
|
// Linearly interpolate the angle some amount.
|
|
var percent = elapsed / KEY_ANIMATION_DURATION;
|
|
this[angleName] = startAngle + (targetAngle - startAngle) * percent;
|
|
}.bind(this));
|
|
};
|
|
|
|
MouseKeyboardVRDisplay.prototype.onMouseDown_ = function(e) {
|
|
this.rotateStart_.set(e.clientX, e.clientY);
|
|
this.isDragging_ = true;
|
|
};
|
|
|
|
// Very similar to https://gist.github.com/mrflix/8351020
|
|
MouseKeyboardVRDisplay.prototype.onMouseMove_ = function(e) {
|
|
if (!this.isDragging_ && !this.isPointerLocked_()) {
|
|
return;
|
|
}
|
|
// Support pointer lock API.
|
|
if (this.isPointerLocked_()) {
|
|
var movementX = e.movementX || e.mozMovementX || 0;
|
|
var movementY = e.movementY || e.mozMovementY || 0;
|
|
this.rotateEnd_.set(this.rotateStart_.x - movementX, this.rotateStart_.y - movementY);
|
|
} else {
|
|
this.rotateEnd_.set(e.clientX, e.clientY);
|
|
}
|
|
// Calculate how much we moved in mouse space.
|
|
this.rotateDelta_.subVectors(this.rotateEnd_, this.rotateStart_);
|
|
this.rotateStart_.copy(this.rotateEnd_);
|
|
|
|
// Keep track of the cumulative euler angles.
|
|
this.phi_ += 2 * Math.PI * this.rotateDelta_.y / screen.height * MOUSE_SPEED_Y;
|
|
this.theta_ += 2 * Math.PI * this.rotateDelta_.x / screen.width * MOUSE_SPEED_X;
|
|
|
|
// Prevent looking too far up or down.
|
|
this.phi_ = Util.clamp(this.phi_, -Math.PI/2, Math.PI/2);
|
|
};
|
|
|
|
MouseKeyboardVRDisplay.prototype.onMouseUp_ = function(e) {
|
|
this.isDragging_ = false;
|
|
};
|
|
|
|
MouseKeyboardVRDisplay.prototype.isPointerLocked_ = function() {
|
|
var el = document.pointerLockElement || document.mozPointerLockElement ||
|
|
document.webkitPointerLockElement;
|
|
return el !== undefined;
|
|
};
|
|
|
|
MouseKeyboardVRDisplay.prototype.resetPose = function() {
|
|
this.phi_ = 0;
|
|
this.theta_ = 0;
|
|
};
|
|
|
|
module.exports = MouseKeyboardVRDisplay;
|
|
|
|
},{"./base.js":2,"./math-util.js":14,"./util.js":22}],16:[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');
|
|
|
|
function RotateInstructions() {
|
|
this.loadIcon_();
|
|
|
|
var overlay = document.createElement('div');
|
|
var s = overlay.style;
|
|
s.position = 'fixed';
|
|
s.top = 0;
|
|
s.right = 0;
|
|
s.bottom = 0;
|
|
s.left = 0;
|
|
s.backgroundColor = 'gray';
|
|
s.fontFamily = 'sans-serif';
|
|
// Force this to be above the fullscreen canvas, which is at zIndex: 999999.
|
|
s.zIndex = 1000000;
|
|
|
|
var img = document.createElement('img');
|
|
img.src = this.icon;
|
|
var s = img.style;
|
|
s.marginLeft = '25%';
|
|
s.marginTop = '25%';
|
|
s.width = '50%';
|
|
overlay.appendChild(img);
|
|
|
|
var text = document.createElement('div');
|
|
var s = text.style;
|
|
s.textAlign = 'center';
|
|
s.fontSize = '16px';
|
|
s.lineHeight = '24px';
|
|
s.margin = '24px 25%';
|
|
s.width = '50%';
|
|
text.innerHTML = 'Place your phone into your Cardboard viewer.';
|
|
overlay.appendChild(text);
|
|
|
|
var snackbar = document.createElement('div');
|
|
var s = snackbar.style;
|
|
s.backgroundColor = '#CFD8DC';
|
|
s.position = 'fixed';
|
|
s.bottom = 0;
|
|
s.width = '100%';
|
|
s.height = '48px';
|
|
s.padding = '14px 24px';
|
|
s.boxSizing = 'border-box';
|
|
s.color = '#656A6B';
|
|
overlay.appendChild(snackbar);
|
|
|
|
var snackbarText = document.createElement('div');
|
|
snackbarText.style.float = 'left';
|
|
snackbarText.innerHTML = 'No Cardboard viewer?';
|
|
|
|
var snackbarButton = document.createElement('a');
|
|
snackbarButton.href = 'https://www.google.com/get/cardboard/get-cardboard/';
|
|
snackbarButton.innerHTML = 'get one';
|
|
snackbarButton.target = '_blank';
|
|
var s = snackbarButton.style;
|
|
s.float = 'right';
|
|
s.fontWeight = 600;
|
|
s.textTransform = 'uppercase';
|
|
s.borderLeft = '1px solid gray';
|
|
s.paddingLeft = '24px';
|
|
s.textDecoration = 'none';
|
|
s.color = '#656A6B';
|
|
|
|
snackbar.appendChild(snackbarText);
|
|
snackbar.appendChild(snackbarButton);
|
|
|
|
this.overlay = overlay;
|
|
this.text = text;
|
|
|
|
this.hide();
|
|
}
|
|
|
|
RotateInstructions.prototype.show = function(parent) {
|
|
if (!parent && !this.overlay.parentElement) {
|
|
document.body.appendChild(this.overlay);
|
|
} else if (parent) {
|
|
if (this.overlay.parentElement && this.overlay.parentElement != parent)
|
|
this.overlay.parentElement.removeChild(this.overlay);
|
|
|
|
parent.appendChild(this.overlay);
|
|
}
|
|
|
|
this.overlay.style.display = 'block';
|
|
|
|
var img = this.overlay.querySelector('img');
|
|
var s = img.style;
|
|
|
|
if (Util.isLandscapeMode()) {
|
|
s.width = '20%';
|
|
s.marginLeft = '40%';
|
|
s.marginTop = '3%';
|
|
} else {
|
|
s.width = '50%';
|
|
s.marginLeft = '25%';
|
|
s.marginTop = '25%';
|
|
}
|
|
};
|
|
|
|
RotateInstructions.prototype.hide = function() {
|
|
this.overlay.style.display = 'none';
|
|
};
|
|
|
|
RotateInstructions.prototype.showTemporarily = function(ms, parent) {
|
|
this.show(parent);
|
|
this.timer = setTimeout(this.hide.bind(this), ms);
|
|
};
|
|
|
|
RotateInstructions.prototype.disableShowTemporarily = function() {
|
|
clearTimeout(this.timer);
|
|
};
|
|
|
|
RotateInstructions.prototype.update = function() {
|
|
this.disableShowTemporarily();
|
|
// In portrait VR mode, tell the user to rotate to landscape. Otherwise, hide
|
|
// the instructions.
|
|
if (!Util.isLandscapeMode() && Util.isMobile()) {
|
|
this.show();
|
|
} else {
|
|
this.hide();
|
|
}
|
|
};
|
|
|
|
RotateInstructions.prototype.loadIcon_ = function() {
|
|
// Encoded asset_src/rotate-instructions.svg
|
|
this.icon = Util.base64('image/svg+xml', 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+Cjxzdmcgd2lkdGg9IjE5OHB4IiBoZWlnaHQ9IjI0MHB4IiB2aWV3Qm94PSIwIDAgMTk4IDI0MCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDMuMy4zICgxMjA4MSkgLSBodHRwOi8vd3d3LmJvaGVtaWFuY29kaW5nLmNvbS9za2V0Y2ggLS0+CiAgICA8dGl0bGU+dHJhbnNpdGlvbjwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHNrZXRjaDp0eXBlPSJNU1BhZ2UiPgogICAgICAgIDxnIGlkPSJ0cmFuc2l0aW9uIiBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIj4KICAgICAgICAgICAgPGcgaWQ9IkltcG9ydGVkLUxheWVycy1Db3B5LTQtKy1JbXBvcnRlZC1MYXllcnMtQ29weS0rLUltcG9ydGVkLUxheWVycy1Db3B5LTItQ29weSIgc2tldGNoOnR5cGU9Ik1TTGF5ZXJHcm91cCI+CiAgICAgICAgICAgICAgICA8ZyBpZD0iSW1wb3J0ZWQtTGF5ZXJzLUNvcHktNCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMC4wMDAwMDAsIDEwNy4wMDAwMDApIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTQ5LjYyNSwyLjUyNyBDMTQ5LjYyNSwyLjUyNyAxNTUuODA1LDYuMDk2IDE1Ni4zNjIsNi40MTggTDE1Ni4zNjIsNy4zMDQgQzE1Ni4zNjIsNy40ODEgMTU2LjM3NSw3LjY2NCAxNTYuNCw3Ljg1MyBDMTU2LjQxLDcuOTM0IDE1Ni40Miw4LjAxNSAxNTYuNDI3LDguMDk1IEMxNTYuNTY3LDkuNTEgMTU3LjQwMSwxMS4wOTMgMTU4LjUzMiwxMi4wOTQgTDE2NC4yNTIsMTcuMTU2IEwxNjQuMzMzLDE3LjA2NiBDMTY0LjMzMywxNy4wNjYgMTY4LjcxNSwxNC41MzYgMTY5LjU2OCwxNC4wNDIgQzE3MS4wMjUsMTQuODgzIDE5NS41MzgsMjkuMDM1IDE5NS41MzgsMjkuMDM1IEwxOTUuNTM4LDgzLjAzNiBDMTk1LjUzOCw4My44MDcgMTk1LjE1Miw4NC4yNTMgMTk0LjU5LDg0LjI1MyBDMTk0LjM1Nyw4NC4yNTMgMTk0LjA5NSw4NC4xNzcgMTkzLjgxOCw4NC4wMTcgTDE2OS44NTEsNzAuMTc5IEwxNjkuODM3LDcwLjIwMyBMMTQyLjUxNSw4NS45NzggTDE0MS42NjUsODQuNjU1IEMxMzYuOTM0LDgzLjEyNiAxMzEuOTE3LDgxLjkxNSAxMjYuNzE0LDgxLjA0NSBDMTI2LjcwOSw4MS4wNiAxMjYuNzA3LDgxLjA2OSAxMjYuNzA3LDgxLjA2OSBMMTIxLjY0LDk4LjAzIEwxMTMuNzQ5LDEwMi41ODYgTDExMy43MTIsMTAyLjUyMyBMMTEzLjcxMiwxMzAuMTEzIEMxMTMuNzEyLDEzMC44ODUgMTEzLjMyNiwxMzEuMzMgMTEyLjc2NCwxMzEuMzMgQzExMi41MzIsMTMxLjMzIDExMi4yNjksMTMxLjI1NCAxMTEuOTkyLDEzMS4wOTQgTDY5LjUxOSwxMDYuNTcyIEM2OC41NjksMTA2LjAyMyA2Ny43OTksMTA0LjY5NSA2Ny43OTksMTAzLjYwNSBMNjcuNzk5LDEwMi41NyBMNjcuNzc4LDEwMi42MTcgQzY3LjI3LDEwMi4zOTMgNjYuNjQ4LDEwMi4yNDkgNjUuOTYyLDEwMi4yMTggQzY1Ljg3NSwxMDIuMjE0IDY1Ljc4OCwxMDIuMjEyIDY1LjcwMSwxMDIuMjEyIEM2NS42MDYsMTAyLjIxMiA2NS41MTEsMTAyLjIxNSA2NS40MTYsMTAyLjIxOSBDNjUuMTk1LDEwMi4yMjkgNjQuOTc0LDEwMi4yMzUgNjQuNzU0LDEwMi4yMzUgQzY0LjMzMSwxMDIuMjM1IDYzLjkxMSwxMDIuMjE2IDYzLjQ5OCwxMDIuMTc4IEM2MS44NDMsMTAyLjAyNSA2MC4yOTgsMTAxLjU3OCA1OS4wOTQsMTAwLjg4MiBMMTIuNTE4LDczLjk5MiBMMTIuNTIzLDc0LjAwNCBMMi4yNDUsNTUuMjU0IEMxLjI0NCw1My40MjcgMi4wMDQsNTEuMDM4IDMuOTQzLDQ5LjkxOCBMNTkuOTU0LDE3LjU3MyBDNjAuNjI2LDE3LjE4NSA2MS4zNSwxNy4wMDEgNjIuMDUzLDE3LjAwMSBDNjMuMzc5LDE3LjAwMSA2NC42MjUsMTcuNjYgNjUuMjgsMTguODU0IEw2NS4yODUsMTguODUxIEw2NS41MTIsMTkuMjY0IEw2NS41MDYsMTkuMjY4IEM2NS45MDksMjAuMDAzIDY2LjQwNSwyMC42OCA2Ni45ODMsMjEuMjg2IEw2Ny4yNiwyMS41NTYgQzY5LjE3NCwyMy40MDYgNzEuNzI4LDI0LjM1NyA3NC4zNzMsMjQuMzU3IEM3Ni4zMjIsMjQuMzU3IDc4LjMyMSwyMy44NCA4MC4xNDgsMjIuNzg1IEM4MC4xNjEsMjIuNzg1IDg3LjQ2NywxOC41NjYgODcuNDY3LDE4LjU2NiBDODguMTM5LDE4LjE3OCA4OC44NjMsMTcuOTk0IDg5LjU2NiwxNy45OTQgQzkwLjg5MiwxNy45OTQgOTIuMTM4LDE4LjY1MiA5Mi43OTIsMTkuODQ3IEw5Ni4wNDIsMjUuNzc1IEw5Ni4wNjQsMjUuNzU3IEwxMDIuODQ5LDI5LjY3NCBMMTAyLjc0NCwyOS40OTIgTDE0OS42MjUsMi41MjcgTTE0OS42MjUsMC44OTIgQzE0OS4zNDMsMC44OTIgMTQ5LjA2MiwwLjk2NSAxNDguODEsMS4xMSBMMTAyLjY0MSwyNy42NjYgTDk3LjIzMSwyNC41NDIgTDk0LjIyNiwxOS4wNjEgQzkzLjMxMywxNy4zOTQgOTEuNTI3LDE2LjM1OSA4OS41NjYsMTYuMzU4IEM4OC41NTUsMTYuMzU4IDg3LjU0NiwxNi42MzIgODYuNjQ5LDE3LjE1IEM4My44NzgsMTguNzUgNzkuNjg3LDIxLjE2OSA3OS4zNzQsMjEuMzQ1IEM3OS4zNTksMjEuMzUzIDc5LjM0NSwyMS4zNjEgNzkuMzMsMjEuMzY5IEM3Ny43OTgsMjIuMjU0IDc2LjA4NCwyMi43MjIgNzQuMzczLDIyLjcyMiBDNzIuMDgxLDIyLjcyMiA2OS45NTksMjEuODkgNjguMzk3LDIwLjM4IEw2OC4xNDUsMjAuMTM1IEM2Ny43MDYsMTkuNjcyIDY3LjMyMywxOS4xNTYgNjcuMDA2LDE4LjYwMSBDNjYuOTg4LDE4LjU1OSA2Ni45NjgsMTguNTE5IDY2Ljk0NiwxOC40NzkgTDY2LjcxOSwxOC4wNjUgQzY2LjY5LDE4LjAxMiA2Ni42NTgsMTcuOTYgNjYuNjI0LDE3LjkxMSBDNjUuNjg2LDE2LjMzNyA2My45NTEsMTUuMzY2IDYyLjA1MywxNS4zNjYgQzYxLjA0MiwxNS4zNjYgNjAuMDMzLDE1LjY0IDU5LjEzNiwxNi4xNTggTDMuMTI1LDQ4LjUwMiBDMC40MjYsNTAuMDYxIC0wLjYxMyw1My40NDIgMC44MTEsNTYuMDQgTDExLjA4OSw3NC43OSBDMTEuMjY2LDc1LjExMyAxMS41MzcsNzUuMzUzIDExLjg1LDc1LjQ5NCBMNTguMjc2LDEwMi4yOTggQzU5LjY3OSwxMDMuMTA4IDYxLjQzMywxMDMuNjMgNjMuMzQ4LDEwMy44MDYgQzYzLjgxMiwxMDMuODQ4IDY0LjI4NSwxMDMuODcgNjQuNzU0LDEwMy44NyBDNjUsMTAzLjg3IDY1LjI0OSwxMDMuODY0IDY1LjQ5NCwxMDMuODUyIEM2NS41NjMsMTAzLjg0OSA2NS42MzIsMTAzLjg0NyA2NS43MDEsMTAzLjg0NyBDNjUuNzY0LDEwMy44NDcgNjUuODI4LDEwMy44NDkgNjUuODksMTAzLjg1MiBDNjUuOTg2LDEwMy44NTYgNjYuMDgsMTAzLjg2MyA2Ni4xNzMsMTAzLjg3NCBDNjYuMjgyLDEwNS40NjcgNjcuMzMyLDEwNy4xOTcgNjguNzAyLDEwNy45ODggTDExMS4xNzQsMTMyLjUxIEMxMTEuNjk4LDEzMi44MTIgMTEyLjIzMiwxMzIuOTY1IDExMi43NjQsMTMyLjk2NSBDMTE0LjI2MSwxMzIuOTY1IDExNS4zNDcsMTMxLjc2NSAxMTUuMzQ3LDEzMC4xMTMgTDExNS4zNDcsMTAzLjU1MSBMMTIyLjQ1OCw5OS40NDYgQzEyMi44MTksOTkuMjM3IDEyMy4wODcsOTguODk4IDEyMy4yMDcsOTguNDk4IEwxMjcuODY1LDgyLjkwNSBDMTMyLjI3OSw4My43MDIgMTM2LjU1Nyw4NC43NTMgMTQwLjYwNyw4Ni4wMzMgTDE0MS4xNCw4Ni44NjIgQzE0MS40NTEsODcuMzQ2IDE0MS45NzcsODcuNjEzIDE0Mi41MTYsODcuNjEzIEMxNDIuNzk0LDg3LjYxMyAxNDMuMDc2LDg3LjU0MiAxNDMuMzMzLDg3LjM5MyBMMTY5Ljg2NSw3Mi4wNzYgTDE5Myw4NS40MzMgQzE5My41MjMsODUuNzM1IDE5NC4wNTgsODUuODg4IDE5NC41OSw4NS44ODggQzE5Ni4wODcsODUuODg4IDE5Ny4xNzMsODQuNjg5IDE5Ny4xNzMsODMuMDM2IEwxOTcuMTczLDI5LjAzNSBDMTk3LjE3MywyOC40NTEgMTk2Ljg2MSwyNy45MTEgMTk2LjM1NSwyNy42MTkgQzE5Ni4zNTUsMjcuNjE5IDE3MS44NDMsMTMuNDY3IDE3MC4zODUsMTIuNjI2IEMxNzAuMTMyLDEyLjQ4IDE2OS44NSwxMi40MDcgMTY5LjU2OCwxMi40MDcgQzE2OS4yODUsMTIuNDA3IDE2OS4wMDIsMTIuNDgxIDE2OC43NDksMTIuNjI3IEMxNjguMTQzLDEyLjk3OCAxNjUuNzU2LDE0LjM1NyAxNjQuNDI0LDE1LjEyNSBMMTU5LjYxNSwxMC44NyBDMTU4Ljc5NiwxMC4xNDUgMTU4LjE1NCw4LjkzNyAxNTguMDU0LDcuOTM0IEMxNTguMDQ1LDcuODM3IDE1OC4wMzQsNy43MzkgMTU4LjAyMSw3LjY0IEMxNTguMDA1LDcuNTIzIDE1Ny45OTgsNy40MSAxNTcuOTk4LDcuMzA0IEwxNTcuOTk4LDYuNDE4IEMxNTcuOTk4LDUuODM0IDE1Ny42ODYsNS4yOTUgMTU3LjE4MSw1LjAwMiBDMTU2LjYyNCw0LjY4IDE1MC40NDIsMS4xMTEgMTUwLjQ0MiwxLjExMSBDMTUwLjE4OSwwLjk2NSAxNDkuOTA3LDAuODkyIDE0OS42MjUsMC44OTIiIGlkPSJGaWxsLTEiIGZpbGw9IiM0NTVBNjQiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNOTYuMDI3LDI1LjYzNiBMMTQyLjYwMyw1Mi41MjcgQzE0My44MDcsNTMuMjIyIDE0NC41ODIsNTQuMTE0IDE0NC44NDUsNTUuMDY4IEwxNDQuODM1LDU1LjA3NSBMNjMuNDYxLDEwMi4wNTcgTDYzLjQ2LDEwMi4wNTcgQzYxLjgwNiwxMDEuOTA1IDYwLjI2MSwxMDEuNDU3IDU5LjA1NywxMDAuNzYyIEwxMi40ODEsNzMuODcxIEw5Ni4wMjcsMjUuNjM2IiBpZD0iRmlsbC0yIiBmaWxsPSIjRkFGQUZBIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTYzLjQ2MSwxMDIuMTc0IEM2My40NTMsMTAyLjE3NCA2My40NDYsMTAyLjE3NCA2My40MzksMTAyLjE3MiBDNjEuNzQ2LDEwMi4wMTYgNjAuMjExLDEwMS41NjMgNTguOTk4LDEwMC44NjMgTDEyLjQyMiw3My45NzMgQzEyLjM4Niw3My45NTIgMTIuMzY0LDczLjkxNCAxMi4zNjQsNzMuODcxIEMxMi4zNjQsNzMuODMgMTIuMzg2LDczLjc5MSAxMi40MjIsNzMuNzcgTDk1Ljk2OCwyNS41MzUgQzk2LjAwNCwyNS41MTQgOTYuMDQ5LDI1LjUxNCA5Ni4wODUsMjUuNTM1IEwxNDIuNjYxLDUyLjQyNiBDMTQzLjg4OCw1My4xMzQgMTQ0LjY4Miw1NC4wMzggMTQ0Ljk1Nyw1NS4wMzcgQzE0NC45Nyw1NS4wODMgMTQ0Ljk1Myw1NS4xMzMgMTQ0LjkxNSw1NS4xNjEgQzE0NC45MTEsNTUuMTY1IDE0NC44OTgsNTUuMTc0IDE0NC44OTQsNTUuMTc3IEw2My41MTksMTAyLjE1OCBDNjMuNTAxLDEwMi4xNjkgNjMuNDgxLDEwMi4xNzQgNjMuNDYxLDEwMi4xNzQgTDYzLjQ2MSwxMDIuMTc0IFogTTEyLjcxNCw3My44NzEgTDU5LjExNSwxMDAuNjYxIEM2MC4yOTMsMTAxLjM0MSA2MS43ODYsMTAxLjc4MiA2My40MzUsMTAxLjkzNyBMMTQ0LjcwNyw1NS4wMTUgQzE0NC40MjgsNTQuMTA4IDE0My42ODIsNTMuMjg1IDE0Mi41NDQsNTIuNjI4IEw5Ni4wMjcsMjUuNzcxIEwxMi43MTQsNzMuODcxIEwxMi43MTQsNzMuODcxIFoiIGlkPSJGaWxsLTMiIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTQ4LjMyNyw1OC40NzEgQzE0OC4xNDUsNTguNDggMTQ3Ljk2Miw1OC40OCAxNDcuNzgxLDU4LjQ3MiBDMTQ1Ljg4Nyw1OC4zODkgMTQ0LjQ3OSw1Ny40MzQgMTQ0LjYzNiw1Ni4zNCBDMTQ0LjY4OSw1NS45NjcgMTQ0LjY2NCw1NS41OTcgMTQ0LjU2NCw1NS4yMzUgTDYzLjQ2MSwxMDIuMDU3IEM2NC4wODksMTAyLjExNSA2NC43MzMsMTAyLjEzIDY1LjM3OSwxMDIuMDk5IEM2NS41NjEsMTAyLjA5IDY1Ljc0MywxMDIuMDkgNjUuOTI1LDEwMi4wOTggQzY3LjgxOSwxMDIuMTgxIDY5LjIyNywxMDMuMTM2IDY5LjA3LDEwNC4yMyBMMTQ4LjMyNyw1OC40NzEiIGlkPSJGaWxsLTQiIGZpbGw9IiNGRkZGRkYiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNNjkuMDcsMTA0LjM0NyBDNjkuMDQ4LDEwNC4zNDcgNjkuMDI1LDEwNC4zNCA2OS4wMDUsMTA0LjMyNyBDNjguOTY4LDEwNC4zMDEgNjguOTQ4LDEwNC4yNTcgNjguOTU1LDEwNC4yMTMgQzY5LDEwMy44OTYgNjguODk4LDEwMy41NzYgNjguNjU4LDEwMy4yODggQzY4LjE1MywxMDIuNjc4IDY3LjEwMywxMDIuMjY2IDY1LjkyLDEwMi4yMTQgQzY1Ljc0MiwxMDIuMjA2IDY1LjU2MywxMDIuMjA3IDY1LjM4NSwxMDIuMjE1IEM2NC43NDIsMTAyLjI0NiA2NC4wODcsMTAyLjIzMiA2My40NSwxMDIuMTc0IEM2My4zOTksMTAyLjE2OSA2My4zNTgsMTAyLjEzMiA2My4zNDcsMTAyLjA4MiBDNjMuMzM2LDEwMi4wMzMgNjMuMzU4LDEwMS45ODEgNjMuNDAyLDEwMS45NTYgTDE0NC41MDYsNTUuMTM0IEMxNDQuNTM3LDU1LjExNiAxNDQuNTc1LDU1LjExMyAxNDQuNjA5LDU1LjEyNyBDMTQ0LjY0Miw1NS4xNDEgMTQ0LjY2OCw1NS4xNyAxNDQuNjc3LDU1LjIwNCBDMTQ0Ljc4MSw1NS41ODUgMTQ0LjgwNiw1NS45NzIgMTQ0Ljc1MSw1Ni4zNTcgQzE0NC43MDYsNTYuNjczIDE0NC44MDgsNTYuOTk0IDE0NS4wNDcsNTcuMjgyIEMxNDUuNTUzLDU3Ljg5MiAxNDYuNjAyLDU4LjMwMyAxNDcuNzg2LDU4LjM1NSBDMTQ3Ljk2NCw1OC4zNjMgMTQ4LjE0Myw1OC4zNjMgMTQ4LjMyMSw1OC4zNTQgQzE0OC4zNzcsNTguMzUyIDE0OC40MjQsNTguMzg3IDE0OC40MzksNTguNDM4IEMxNDguNDU0LDU4LjQ5IDE0OC40MzIsNTguNTQ1IDE0OC4zODUsNTguNTcyIEw2OS4xMjksMTA0LjMzMSBDNjkuMTExLDEwNC4zNDIgNjkuMDksMTA0LjM0NyA2OS4wNywxMDQuMzQ3IEw2OS4wNywxMDQuMzQ3IFogTTY1LjY2NSwxMDEuOTc1IEM2NS43NTQsMTAxLjk3NSA2NS44NDIsMTAxLjk3NyA2NS45MywxMDEuOTgxIEM2Ny4xOTYsMTAyLjAzNyA2OC4yODMsMTAyLjQ2OSA2OC44MzgsMTAzLjEzOSBDNjkuMDY1LDEwMy40MTMgNjkuMTg4LDEwMy43MTQgNjkuMTk4LDEwNC4wMjEgTDE0Ny44ODMsNTguNTkyIEMxNDcuODQ3LDU4LjU5MiAxNDcuODExLDU4LjU5MSAxNDcuNzc2LDU4LjU4OSBDMTQ2LjUwOSw1OC41MzMgMTQ1LjQyMiw1OC4xIDE0NC44NjcsNTcuNDMxIEMxNDQuNTg1LDU3LjA5MSAxNDQuNDY1LDU2LjcwNyAxNDQuNTIsNTYuMzI0IEMxNDQuNTYzLDU2LjAyMSAxNDQuNTUyLDU1LjcxNiAxNDQuNDg4LDU1LjQxNCBMNjMuODQ2LDEwMS45NyBDNjQuMzUzLDEwMi4wMDIgNjQuODY3LDEwMi4wMDYgNjUuMzc0LDEwMS45ODIgQzY1LjQ3MSwxMDEuOTc3IDY1LjU2OCwxMDEuOTc1IDY1LjY2NSwxMDEuOTc1IEw2NS42NjUsMTAxLjk3NSBaIiBpZD0iRmlsbC01IiBmaWxsPSIjNjA3RDhCIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTIuMjA4LDU1LjEzNCBDMS4yMDcsNTMuMzA3IDEuOTY3LDUwLjkxNyAzLjkwNiw0OS43OTcgTDU5LjkxNywxNy40NTMgQzYxLjg1NiwxNi4zMzMgNjQuMjQxLDE2LjkwNyA2NS4yNDMsMTguNzM0IEw2NS40NzUsMTkuMTQ0IEM2NS44NzIsMTkuODgyIDY2LjM2OCwyMC41NiA2Ni45NDUsMjEuMTY1IEw2Ny4yMjMsMjEuNDM1IEM3MC41NDgsMjQuNjQ5IDc1LjgwNiwyNS4xNTEgODAuMTExLDIyLjY2NSBMODcuNDMsMTguNDQ1IEM4OS4zNywxNy4zMjYgOTEuNzU0LDE3Ljg5OSA5Mi43NTUsMTkuNzI3IEw5Ni4wMDUsMjUuNjU1IEwxMi40ODYsNzMuODg0IEwyLjIwOCw1NS4xMzQgWiIgaWQ9IkZpbGwtNiIgZmlsbD0iI0ZBRkFGQSI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xMi40ODYsNzQuMDAxIEMxMi40NzYsNzQuMDAxIDEyLjQ2NSw3My45OTkgMTIuNDU1LDczLjk5NiBDMTIuNDI0LDczLjk4OCAxMi4zOTksNzMuOTY3IDEyLjM4NCw3My45NCBMMi4xMDYsNTUuMTkgQzEuMDc1LDUzLjMxIDEuODU3LDUwLjg0NSAzLjg0OCw0OS42OTYgTDU5Ljg1OCwxNy4zNTIgQzYwLjUyNSwxNi45NjcgNjEuMjcxLDE2Ljc2NCA2Mi4wMTYsMTYuNzY0IEM2My40MzEsMTYuNzY0IDY0LjY2NiwxNy40NjYgNjUuMzI3LDE4LjY0NiBDNjUuMzM3LDE4LjY1NCA2NS4zNDUsMTguNjYzIDY1LjM1MSwxOC42NzQgTDY1LjU3OCwxOS4wODggQzY1LjU4NCwxOS4xIDY1LjU4OSwxOS4xMTIgNjUuNTkxLDE5LjEyNiBDNjUuOTg1LDE5LjgzOCA2Ni40NjksMjAuNDk3IDY3LjAzLDIxLjA4NSBMNjcuMzA1LDIxLjM1MSBDNjkuMTUxLDIzLjEzNyA3MS42NDksMjQuMTIgNzQuMzM2LDI0LjEyIEM3Ni4zMTMsMjQuMTIgNzguMjksMjMuNTgyIDgwLjA1MywyMi41NjMgQzgwLjA2NCwyMi41NTcgODAuMDc2LDIyLjU1MyA4MC4wODgsMjIuNTUgTDg3LjM3MiwxOC4zNDQgQzg4LjAzOCwxNy45NTkgODguNzg0LDE3Ljc1NiA4OS41MjksMTcuNzU2IEM5MC45NTYsMTcuNzU2IDkyLjIwMSwxOC40NzIgOTIuODU4LDE5LjY3IEw5Ni4xMDcsMjUuNTk5IEM5Ni4xMzgsMjUuNjU0IDk2LjExOCwyNS43MjQgOTYuMDYzLDI1Ljc1NiBMMTIuNTQ1LDczLjk4NSBDMTIuNTI2LDczLjk5NiAxMi41MDYsNzQuMDAxIDEyLjQ4Niw3NC4wMDEgTDEyLjQ4Niw3NC4wMDEgWiBNNjIuMDE2LDE2Ljk5NyBDNjEuMzEyLDE2Ljk5NyA2MC42MDYsMTcuMTkgNTkuOTc1LDE3LjU1NCBMMy45NjUsNDkuODk5IEMyLjA4Myw1MC45ODUgMS4zNDEsNTMuMzA4IDIuMzEsNTUuMDc4IEwxMi41MzEsNzMuNzIzIEw5NS44NDgsMjUuNjExIEw5Mi42NTMsMTkuNzgyIEM5Mi4wMzgsMTguNjYgOTAuODcsMTcuOTkgODkuNTI5LDE3Ljk5IEM4OC44MjUsMTcuOTkgODguMTE5LDE4LjE4MiA4Ny40ODksMTguNTQ3IEw4MC4xNzIsMjIuNzcyIEM4MC4xNjEsMjIuNzc4IDgwLjE0OSwyMi43ODIgODAuMTM3LDIyLjc4NSBDNzguMzQ2LDIzLjgxMSA3Ni4zNDEsMjQuMzU0IDc0LjMzNiwyNC4zNTQgQzcxLjU4OCwyNC4zNTQgNjkuMDMzLDIzLjM0NyA2Ny4xNDIsMjEuNTE5IEw2Ni44NjQsMjEuMjQ5IEM2Ni4yNzcsMjAuNjM0IDY1Ljc3NCwxOS45NDcgNjUuMzY3LDE5LjIwMyBDNjUuMzYsMTkuMTkyIDY1LjM1NiwxOS4xNzkgNjUuMzU0LDE5LjE2NiBMNjUuMTYzLDE4LjgxOSBDNjUuMTU0LDE4LjgxMSA2NS4xNDYsMTguODAxIDY1LjE0LDE4Ljc5IEM2NC41MjUsMTcuNjY3IDYzLjM1NywxNi45OTcgNjIuMDE2LDE2Ljk5NyBMNjIuMDE2LDE2Ljk5NyBaIiBpZD0iRmlsbC03IiBmaWxsPSIjNjA3RDhCIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTQyLjQzNCw0OC44MDggTDQyLjQzNCw0OC44MDggQzM5LjkyNCw0OC44MDcgMzcuNzM3LDQ3LjU1IDM2LjU4Miw0NS40NDMgQzM0Ljc3MSw0Mi4xMzkgMzYuMTQ0LDM3LjgwOSAzOS42NDEsMzUuNzg5IEw1MS45MzIsMjguNjkxIEM1My4xMDMsMjguMDE1IDU0LjQxMywyNy42NTggNTUuNzIxLDI3LjY1OCBDNTguMjMxLDI3LjY1OCA2MC40MTgsMjguOTE2IDYxLjU3MywzMS4wMjMgQzYzLjM4NCwzNC4zMjcgNjIuMDEyLDM4LjY1NyA1OC41MTQsNDAuNjc3IEw0Ni4yMjMsNDcuNzc1IEM0NS4wNTMsNDguNDUgNDMuNzQyLDQ4LjgwOCA0Mi40MzQsNDguODA4IEw0Mi40MzQsNDguODA4IFogTTU1LjcyMSwyOC4xMjUgQzU0LjQ5NSwyOC4xMjUgNTMuMjY1LDI4LjQ2MSA1Mi4xNjYsMjkuMDk2IEwzOS44NzUsMzYuMTk0IEMzNi41OTYsMzguMDg3IDM1LjMwMiw0Mi4xMzYgMzYuOTkyLDQ1LjIxOCBDMzguMDYzLDQ3LjE3MyA0MC4wOTgsNDguMzQgNDIuNDM0LDQ4LjM0IEM0My42NjEsNDguMzQgNDQuODksNDguMDA1IDQ1Ljk5LDQ3LjM3IEw1OC4yODEsNDAuMjcyIEM2MS41NiwzOC4zNzkgNjIuODUzLDM0LjMzIDYxLjE2NCwzMS4yNDggQzYwLjA5MiwyOS4yOTMgNTguMDU4LDI4LjEyNSA1NS43MjEsMjguMTI1IEw1NS43MjEsMjguMTI1IFoiIGlkPSJGaWxsLTgiIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTQ5LjU4OCwyLjQwNyBDMTQ5LjU4OCwyLjQwNyAxNTUuNzY4LDUuOTc1IDE1Ni4zMjUsNi4yOTcgTDE1Ni4zMjUsNy4xODQgQzE1Ni4zMjUsNy4zNiAxNTYuMzM4LDcuNTQ0IDE1Ni4zNjIsNy43MzMgQzE1Ni4zNzMsNy44MTQgMTU2LjM4Miw3Ljg5NCAxNTYuMzksNy45NzUgQzE1Ni41Myw5LjM5IDE1Ny4zNjMsMTAuOTczIDE1OC40OTUsMTEuOTc0IEwxNjUuODkxLDE4LjUxOSBDMTY2LjA2OCwxOC42NzUgMTY2LjI0OSwxOC44MTQgMTY2LjQzMiwxOC45MzQgQzE2OC4wMTEsMTkuOTc0IDE2OS4zODIsMTkuNCAxNjkuNDk0LDE3LjY1MiBDMTY5LjU0MywxNi44NjggMTY5LjU1MSwxNi4wNTcgMTY5LjUxNywxNS4yMjMgTDE2OS41MTQsMTUuMDYzIEwxNjkuNTE0LDEzLjkxMiBDMTcwLjc4LDE0LjY0MiAxOTUuNTAxLDI4LjkxNSAxOTUuNTAxLDI4LjkxNSBMMTk1LjUwMSw4Mi45MTUgQzE5NS41MDEsODQuMDA1IDE5NC43MzEsODQuNDQ1IDE5My43ODEsODMuODk3IEwxNTEuMzA4LDU5LjM3NCBDMTUwLjM1OCw1OC44MjYgMTQ5LjU4OCw1Ny40OTcgMTQ5LjU4OCw1Ni40MDggTDE0OS41ODgsMjIuMzc1IiBpZD0iRmlsbC05IiBmaWxsPSIjRkFGQUZBIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTE5NC41NTMsODQuMjUgQzE5NC4yOTYsODQuMjUgMTk0LjAxMyw4NC4xNjUgMTkzLjcyMiw4My45OTcgTDE1MS4yNSw1OS40NzYgQzE1MC4yNjksNTguOTA5IDE0OS40NzEsNTcuNTMzIDE0OS40NzEsNTYuNDA4IEwxNDkuNDcxLDIyLjM3NSBMMTQ5LjcwNSwyMi4zNzUgTDE0OS43MDUsNTYuNDA4IEMxNDkuNzA1LDU3LjQ1OSAxNTAuNDUsNTguNzQ0IDE1MS4zNjYsNTkuMjc0IEwxOTMuODM5LDgzLjc5NSBDMTk0LjI2Myw4NC4wNCAxOTQuNjU1LDg0LjA4MyAxOTQuOTQyLDgzLjkxNyBDMTk1LjIyNyw4My43NTMgMTk1LjM4NCw4My4zOTcgMTk1LjM4NCw4Mi45MTUgTDE5NS4zODQsMjguOTgyIEMxOTQuMTAyLDI4LjI0MiAxNzIuMTA0LDE1LjU0MiAxNjkuNjMxLDE0LjExNCBMMTY5LjYzNCwxNS4yMiBDMTY5LjY2OCwxNi4wNTIgMTY5LjY2LDE2Ljg3NCAxNjkuNjEsMTcuNjU5IEMxNjkuNTU2LDE4LjUwMyAxNjkuMjE0LDE5LjEyMyAxNjguNjQ3LDE5LjQwNSBDMTY4LjAyOCwxOS43MTQgMTY3LjE5NywxOS41NzggMTY2LjM2NywxOS4wMzIgQzE2Ni4xODEsMTguOTA5IDE2NS45OTUsMTguNzY2IDE2NS44MTQsMTguNjA2IEwxNTguNDE3LDEyLjA2MiBDMTU3LjI1OSwxMS4wMzYgMTU2LjQxOCw5LjQzNyAxNTYuMjc0LDcuOTg2IEMxNTYuMjY2LDcuOTA3IDE1Ni4yNTcsNy44MjcgMTU2LjI0Nyw3Ljc0OCBDMTU2LjIyMSw3LjU1NSAxNTYuMjA5LDcuMzY1IDE1Ni4yMDksNy4xODQgTDE1Ni4yMDksNi4zNjQgQzE1NS4zNzUsNS44ODMgMTQ5LjUyOSwyLjUwOCAxNDkuNTI5LDIuNTA4IEwxNDkuNjQ2LDIuMzA2IEMxNDkuNjQ2LDIuMzA2IDE1NS44MjcsNS44NzQgMTU2LjM4NCw2LjE5NiBMMTU2LjQ0Miw2LjIzIEwxNTYuNDQyLDcuMTg0IEMxNTYuNDQyLDcuMzU1IDE1Ni40NTQsNy41MzUgMTU2LjQ3OCw3LjcxNyBDMTU2LjQ4OSw3LjggMTU2LjQ5OSw3Ljg4MiAxNTYuNTA3LDcuOTYzIEMxNTYuNjQ1LDkuMzU4IDE1Ny40NTUsMTAuODk4IDE1OC41NzIsMTEuODg2IEwxNjUuOTY5LDE4LjQzMSBDMTY2LjE0MiwxOC41ODQgMTY2LjMxOSwxOC43MiAxNjYuNDk2LDE4LjgzNyBDMTY3LjI1NCwxOS4zMzYgMTY4LDE5LjQ2NyAxNjguNTQzLDE5LjE5NiBDMTY5LjAzMywxOC45NTMgMTY5LjMyOSwxOC40MDEgMTY5LjM3NywxNy42NDUgQzE2OS40MjcsMTYuODY3IDE2OS40MzQsMTYuMDU0IDE2OS40MDEsMTUuMjI4IEwxNjkuMzk3LDE1LjA2NSBMMTY5LjM5NywxMy43MSBMMTY5LjU3MiwxMy44MSBDMTcwLjgzOSwxNC41NDEgMTk1LjU1OSwyOC44MTQgMTk1LjU1OSwyOC44MTQgTDE5NS42MTgsMjguODQ3IEwxOTUuNjE4LDgyLjkxNSBDMTk1LjYxOCw4My40ODQgMTk1LjQyLDgzLjkxMSAxOTUuMDU5LDg0LjExOSBDMTk0LjkwOCw4NC4yMDYgMTk0LjczNyw4NC4yNSAxOTQuNTUzLDg0LjI1IiBpZD0iRmlsbC0xMCIgZmlsbD0iIzYwN0Q4QiI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xNDUuNjg1LDU2LjE2MSBMMTY5LjgsNzAuMDgzIEwxNDMuODIyLDg1LjA4MSBMMTQyLjM2LDg0Ljc3NCBDMTM1LjgyNiw4Mi42MDQgMTI4LjczMiw4MS4wNDYgMTIxLjM0MSw4MC4xNTggQzExNi45NzYsNzkuNjM0IDExMi42NzgsODEuMjU0IDExMS43NDMsODMuNzc4IEMxMTEuNTA2LDg0LjQxNCAxMTEuNTAzLDg1LjA3MSAxMTEuNzMyLDg1LjcwNiBDMTEzLjI3LDg5Ljk3MyAxMTUuOTY4LDk0LjA2OSAxMTkuNzI3LDk3Ljg0MSBMMTIwLjI1OSw5OC42ODYgQzEyMC4yNiw5OC42ODUgOTQuMjgyLDExMy42ODMgOTQuMjgyLDExMy42ODMgTDcwLjE2Nyw5OS43NjEgTDE0NS42ODUsNTYuMTYxIiBpZD0iRmlsbC0xMSIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik05NC4yODIsMTEzLjgxOCBMOTQuMjIzLDExMy43ODUgTDY5LjkzMyw5OS43NjEgTDcwLjEwOCw5OS42NiBMMTQ1LjY4NSw1Ni4wMjYgTDE0NS43NDMsNTYuMDU5IEwxNzAuMDMzLDcwLjA4MyBMMTQzLjg0Miw4NS4yMDUgTDE0My43OTcsODUuMTk1IEMxNDMuNzcyLDg1LjE5IDE0Mi4zMzYsODQuODg4IDE0Mi4zMzYsODQuODg4IEMxMzUuNzg3LDgyLjcxNCAxMjguNzIzLDgxLjE2MyAxMjEuMzI3LDgwLjI3NCBDMTIwLjc4OCw4MC4yMDkgMTIwLjIzNiw4MC4xNzcgMTE5LjY4OSw4MC4xNzcgQzExNS45MzEsODAuMTc3IDExMi42MzUsODEuNzA4IDExMS44NTIsODMuODE5IEMxMTEuNjI0LDg0LjQzMiAxMTEuNjIxLDg1LjA1MyAxMTEuODQyLDg1LjY2NyBDMTEzLjM3Nyw4OS45MjUgMTE2LjA1OCw5My45OTMgMTE5LjgxLDk3Ljc1OCBMMTE5LjgyNiw5Ny43NzkgTDEyMC4zNTIsOTguNjE0IEMxMjAuMzU0LDk4LjYxNyAxMjAuMzU2LDk4LjYyIDEyMC4zNTgsOTguNjI0IEwxMjAuNDIyLDk4LjcyNiBMMTIwLjMxNyw5OC43ODcgQzEyMC4yNjQsOTguODE4IDk0LjU5OSwxMTMuNjM1IDk0LjM0LDExMy43ODUgTDk0LjI4MiwxMTMuODE4IEw5NC4yODIsMTEzLjgxOCBaIE03MC40MDEsOTkuNzYxIEw5NC4yODIsMTEzLjU0OSBMMTE5LjA4NCw5OS4yMjkgQzExOS42Myw5OC45MTQgMTE5LjkzLDk4Ljc0IDEyMC4xMDEsOTguNjU0IEwxMTkuNjM1LDk3LjkxNCBDMTE1Ljg2NCw5NC4xMjcgMTEzLjE2OCw5MC4wMzMgMTExLjYyMiw4NS43NDYgQzExMS4zODIsODUuMDc5IDExMS4zODYsODQuNDA0IDExMS42MzMsODMuNzM4IEMxMTIuNDQ4LDgxLjUzOSAxMTUuODM2LDc5Ljk0MyAxMTkuNjg5LDc5Ljk0MyBDMTIwLjI0Niw3OS45NDMgMTIwLjgwNiw3OS45NzYgMTIxLjM1NSw4MC4wNDIgQzEyOC43NjcsODAuOTMzIDEzNS44NDYsODIuNDg3IDE0Mi4zOTYsODQuNjYzIEMxNDMuMjMyLDg0LjgzOCAxNDMuNjExLDg0LjkxNyAxNDMuNzg2LDg0Ljk2NyBMMTY5LjU2Niw3MC4wODMgTDE0NS42ODUsNTYuMjk1IEw3MC40MDEsOTkuNzYxIEw3MC40MDEsOTkuNzYxIFoiIGlkPSJGaWxsLTEyIiBmaWxsPSIjNjA3RDhCIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTE2Ny4yMywxOC45NzkgTDE2Ny4yMyw2OS44NSBMMTM5LjkwOSw4NS42MjMgTDEzMy40NDgsNzEuNDU2IEMxMzIuNTM4LDY5LjQ2IDEzMC4wMiw2OS43MTggMTI3LjgyNCw3Mi4wMyBDMTI2Ljc2OSw3My4xNCAxMjUuOTMxLDc0LjU4NSAxMjUuNDk0LDc2LjA0OCBMMTE5LjAzNCw5Ny42NzYgTDkxLjcxMiwxMTMuNDUgTDkxLjcxMiw2Mi41NzkgTDE2Ny4yMywxOC45NzkiIGlkPSJGaWxsLTEzIiBmaWxsPSIjRkZGRkZGIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTkxLjcxMiwxMTMuNTY3IEM5MS42OTIsMTEzLjU2NyA5MS42NzIsMTEzLjU2MSA5MS42NTMsMTEzLjU1MSBDOTEuNjE4LDExMy41MyA5MS41OTUsMTEzLjQ5MiA5MS41OTUsMTEzLjQ1IEw5MS41OTUsNjIuNTc5IEM5MS41OTUsNjIuNTM3IDkxLjYxOCw2Mi40OTkgOTEuNjUzLDYyLjQ3OCBMMTY3LjE3MiwxOC44NzggQzE2Ny4yMDgsMTguODU3IDE2Ny4yNTIsMTguODU3IDE2Ny4yODgsMTguODc4IEMxNjcuMzI0LDE4Ljg5OSAxNjcuMzQ3LDE4LjkzNyAxNjcuMzQ3LDE4Ljk3OSBMMTY3LjM0Nyw2OS44NSBDMTY3LjM0Nyw2OS44OTEgMTY3LjMyNCw2OS45MyAxNjcuMjg4LDY5Ljk1IEwxMzkuOTY3LDg1LjcyNSBDMTM5LjkzOSw4NS43NDEgMTM5LjkwNSw4NS43NDUgMTM5Ljg3Myw4NS43MzUgQzEzOS44NDIsODUuNzI1IDEzOS44MTYsODUuNzAyIDEzOS44MDIsODUuNjcyIEwxMzMuMzQyLDcxLjUwNCBDMTMyLjk2Nyw3MC42ODIgMTMyLjI4LDcwLjIyOSAxMzEuNDA4LDcwLjIyOSBDMTMwLjMxOSw3MC4yMjkgMTI5LjA0NCw3MC45MTUgMTI3LjkwOCw3Mi4xMSBDMTI2Ljg3NCw3My4yIDEyNi4wMzQsNzQuNjQ3IDEyNS42MDYsNzYuMDgyIEwxMTkuMTQ2LDk3LjcwOSBDMTE5LjEzNyw5Ny43MzggMTE5LjExOCw5Ny43NjIgMTE5LjA5Miw5Ny43NzcgTDkxLjc3LDExMy41NTEgQzkxLjc1MiwxMTMuNTYxIDkxLjczMiwxMTMuNTY3IDkxLjcxMiwxMTMuNTY3IEw5MS43MTIsMTEzLjU2NyBaIE05MS44MjksNjIuNjQ3IEw5MS44MjksMTEzLjI0OCBMMTE4LjkzNSw5Ny41OTggTDEyNS4zODIsNzYuMDE1IEMxMjUuODI3LDc0LjUyNSAxMjYuNjY0LDczLjA4MSAxMjcuNzM5LDcxLjk1IEMxMjguOTE5LDcwLjcwOCAxMzAuMjU2LDY5Ljk5NiAxMzEuNDA4LDY5Ljk5NiBDMTMyLjM3Nyw2OS45OTYgMTMzLjEzOSw3MC40OTcgMTMzLjU1NCw3MS40MDcgTDEzOS45NjEsODUuNDU4IEwxNjcuMTEzLDY5Ljc4MiBMMTY3LjExMywxOS4xODEgTDkxLjgyOSw2Mi42NDcgTDkxLjgyOSw2Mi42NDcgWiIgaWQ9IkZpbGwtMTQiIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTY4LjU0MywxOS4yMTMgTDE2OC41NDMsNzAuMDgzIEwxNDEuMjIxLDg1Ljg1NyBMMTM0Ljc2MSw3MS42ODkgQzEzMy44NTEsNjkuNjk0IDEzMS4zMzMsNjkuOTUxIDEyOS4xMzcsNzIuMjYzIEMxMjguMDgyLDczLjM3NCAxMjcuMjQ0LDc0LjgxOSAxMjYuODA3LDc2LjI4MiBMMTIwLjM0Niw5Ny45MDkgTDkzLjAyNSwxMTMuNjgzIEw5My4wMjUsNjIuODEzIEwxNjguNTQzLDE5LjIxMyIgaWQ9IkZpbGwtMTUiIGZpbGw9IiNGRkZGRkYiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNOTMuMDI1LDExMy44IEM5My4wMDUsMTEzLjggOTIuOTg0LDExMy43OTUgOTIuOTY2LDExMy43ODUgQzkyLjkzMSwxMTMuNzY0IDkyLjkwOCwxMTMuNzI1IDkyLjkwOCwxMTMuNjg0IEw5Mi45MDgsNjIuODEzIEM5Mi45MDgsNjIuNzcxIDkyLjkzMSw2Mi43MzMgOTIuOTY2LDYyLjcxMiBMMTY4LjQ4NCwxOS4xMTIgQzE2OC41MiwxOS4wOSAxNjguNTY1LDE5LjA5IDE2OC42MDEsMTkuMTEyIEMxNjguNjM3LDE5LjEzMiAxNjguNjYsMTkuMTcxIDE2OC42NiwxOS4yMTIgTDE2OC42Niw3MC4wODMgQzE2OC42Niw3MC4xMjUgMTY4LjYzNyw3MC4xNjQgMTY4LjYwMSw3MC4xODQgTDE0MS4yOCw4NS45NTggQzE0MS4yNTEsODUuOTc1IDE0MS4yMTcsODUuOTc5IDE0MS4xODYsODUuOTY4IEMxNDEuMTU0LDg1Ljk1OCAxNDEuMTI5LDg1LjkzNiAxNDEuMTE1LDg1LjkwNiBMMTM0LjY1NSw3MS43MzggQzEzNC4yOCw3MC45MTUgMTMzLjU5Myw3MC40NjMgMTMyLjcyLDcwLjQ2MyBDMTMxLjYzMiw3MC40NjMgMTMwLjM1Nyw3MS4xNDggMTI5LjIyMSw3Mi4zNDQgQzEyOC4xODYsNzMuNDMzIDEyNy4zNDcsNzQuODgxIDEyNi45MTksNzYuMzE1IEwxMjAuNDU4LDk3Ljk0MyBDMTIwLjQ1LDk3Ljk3MiAxMjAuNDMxLDk3Ljk5NiAxMjAuNDA1LDk4LjAxIEw5My4wODMsMTEzLjc4NSBDOTMuMDY1LDExMy43OTUgOTMuMDQ1LDExMy44IDkzLjAyNSwxMTMuOCBMOTMuMDI1LDExMy44IFogTTkzLjE0Miw2Mi44ODEgTDkzLjE0MiwxMTMuNDgxIEwxMjAuMjQ4LDk3LjgzMiBMMTI2LjY5NSw3Ni4yNDggQzEyNy4xNCw3NC43NTggMTI3Ljk3Nyw3My4zMTUgMTI5LjA1Miw3Mi4xODMgQzEzMC4yMzEsNzAuOTQyIDEzMS41NjgsNzAuMjI5IDEzMi43Miw3MC4yMjkgQzEzMy42ODksNzAuMjI5IDEzNC40NTIsNzAuNzMxIDEzNC44NjcsNzEuNjQxIEwxNDEuMjc0LDg1LjY5MiBMMTY4LjQyNiw3MC4wMTYgTDE2OC40MjYsMTkuNDE1IEw5My4xNDIsNjIuODgxIEw5My4xNDIsNjIuODgxIFoiIGlkPSJGaWxsLTE2IiBmaWxsPSIjNjA3RDhCIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTE2OS44LDcwLjA4MyBMMTQyLjQ3OCw4NS44NTcgTDEzNi4wMTgsNzEuNjg5IEMxMzUuMTA4LDY5LjY5NCAxMzIuNTksNjkuOTUxIDEzMC4zOTMsNzIuMjYzIEMxMjkuMzM5LDczLjM3NCAxMjguNSw3NC44MTkgMTI4LjA2NCw3Ni4yODIgTDEyMS42MDMsOTcuOTA5IEw5NC4yODIsMTEzLjY4MyBMOTQuMjgyLDYyLjgxMyBMMTY5LjgsMTkuMjEzIEwxNjkuOCw3MC4wODMgWiIgaWQ9IkZpbGwtMTciIGZpbGw9IiNGQUZBRkEiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNOTQuMjgyLDExMy45MTcgQzk0LjI0MSwxMTMuOTE3IDk0LjIwMSwxMTMuOTA3IDk0LjE2NSwxMTMuODg2IEM5NC4wOTMsMTEzLjg0NSA5NC4wNDgsMTEzLjc2NyA5NC4wNDgsMTEzLjY4NCBMOTQuMDQ4LDYyLjgxMyBDOTQuMDQ4LDYyLjczIDk0LjA5Myw2Mi42NTIgOTQuMTY1LDYyLjYxMSBMMTY5LjY4MywxOS4wMSBDMTY5Ljc1NSwxOC45NjkgMTY5Ljg0NCwxOC45NjkgMTY5LjkxNywxOS4wMSBDMTY5Ljk4OSwxOS4wNTIgMTcwLjAzMywxOS4xMjkgMTcwLjAzMywxOS4yMTIgTDE3MC4wMzMsNzAuMDgzIEMxNzAuMDMzLDcwLjE2NiAxNjkuOTg5LDcwLjI0NCAxNjkuOTE3LDcwLjI4NSBMMTQyLjU5NSw4Ni4wNiBDMTQyLjUzOCw4Ni4wOTIgMTQyLjQ2OSw4Ni4xIDE0Mi40MDcsODYuMDggQzE0Mi4zNDQsODYuMDYgMTQyLjI5Myw4Ni4wMTQgMTQyLjI2Niw4NS45NTQgTDEzNS44MDUsNzEuNzg2IEMxMzUuNDQ1LDcwLjk5NyAxMzQuODEzLDcwLjU4IDEzMy45NzcsNzAuNTggQzEzMi45MjEsNzAuNTggMTMxLjY3Niw3MS4yNTIgMTMwLjU2Miw3Mi40MjQgQzEyOS41NCw3My41MDEgMTI4LjcxMSw3NC45MzEgMTI4LjI4Nyw3Ni4zNDggTDEyMS44MjcsOTcuOTc2IEMxMjEuODEsOTguMDM0IDEyMS43NzEsOTguMDgyIDEyMS43Miw5OC4xMTIgTDk0LjM5OCwxMTMuODg2IEM5NC4zNjIsMTEzLjkwNyA5NC4zMjIsMTEzLjkxNyA5NC4yODIsMTEzLjkxNyBMOTQuMjgyLDExMy45MTcgWiBNOTQuNTE1LDYyLjk0OCBMOTQuNTE1LDExMy4yNzkgTDEyMS40MDYsOTcuNzU0IEwxMjcuODQsNzYuMjE1IEMxMjguMjksNzQuNzA4IDEyOS4xMzcsNzMuMjQ3IDEzMC4yMjQsNzIuMTAzIEMxMzEuNDI1LDcwLjgzOCAxMzIuNzkzLDcwLjExMiAxMzMuOTc3LDcwLjExMiBDMTM0Ljk5NSw3MC4xMTIgMTM1Ljc5NSw3MC42MzggMTM2LjIzLDcxLjU5MiBMMTQyLjU4NCw4NS41MjYgTDE2OS41NjYsNjkuOTQ4IEwxNjkuNTY2LDE5LjYxNyBMOTQuNTE1LDYyLjk0OCBMOTQuNTE1LDYyLjk0OCBaIiBpZD0iRmlsbC0xOCIgZmlsbD0iIzYwN0Q4QiI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xMDkuODk0LDkyLjk0MyBMMTA5Ljg5NCw5Mi45NDMgQzEwOC4xMiw5Mi45NDMgMTA2LjY1Myw5Mi4yMTggMTA1LjY1LDkwLjgyMyBDMTA1LjU4Myw5MC43MzEgMTA1LjU5Myw5MC42MSAxMDUuNjczLDkwLjUyOSBDMTA1Ljc1Myw5MC40NDggMTA1Ljg4LDkwLjQ0IDEwNS45NzQsOTAuNTA2IEMxMDYuNzU0LDkxLjA1MyAxMDcuNjc5LDkxLjMzMyAxMDguNzI0LDkxLjMzMyBDMTEwLjA0Nyw5MS4zMzMgMTExLjQ3OCw5MC44OTQgMTEyLjk4LDkwLjAyNyBDMTE4LjI5MSw4Ni45NiAxMjIuNjExLDc5LjUwOSAxMjIuNjExLDczLjQxNiBDMTIyLjYxMSw3MS40ODkgMTIyLjE2OSw2OS44NTYgMTIxLjMzMyw2OC42OTIgQzEyMS4yNjYsNjguNiAxMjEuMjc2LDY4LjQ3MyAxMjEuMzU2LDY4LjM5MiBDMTIxLjQzNiw2OC4zMTEgMTIxLjU2Myw2OC4yOTkgMTIxLjY1Niw2OC4zNjUgQzEyMy4zMjcsNjkuNTM3IDEyNC4yNDcsNzEuNzQ2IDEyNC4yNDcsNzQuNTg0IEMxMjQuMjQ3LDgwLjgyNiAxMTkuODIxLDg4LjQ0NyAxMTQuMzgyLDkxLjU4NyBDMTEyLjgwOCw5Mi40OTUgMTExLjI5OCw5Mi45NDMgMTA5Ljg5NCw5Mi45NDMgTDEwOS44OTQsOTIuOTQzIFogTTEwNi45MjUsOTEuNDAxIEMxMDcuNzM4LDkyLjA1MiAxMDguNzQ1LDkyLjI3OCAxMDkuODkzLDkyLjI3OCBMMTA5Ljg5NCw5Mi4yNzggQzExMS4yMTUsOTIuMjc4IDExMi42NDcsOTEuOTUxIDExNC4xNDgsOTEuMDg0IEMxMTkuNDU5LDg4LjAxNyAxMjMuNzgsODAuNjIxIDEyMy43OCw3NC41MjggQzEyMy43OCw3Mi41NDkgMTIzLjMxNyw3MC45MjkgMTIyLjQ1NCw2OS43NjcgQzEyMi44NjUsNzAuODAyIDEyMy4wNzksNzIuMDQyIDEyMy4wNzksNzMuNDAyIEMxMjMuMDc5LDc5LjY0NSAxMTguNjUzLDg3LjI4NSAxMTMuMjE0LDkwLjQyNSBDMTExLjY0LDkxLjMzNCAxMTAuMTMsOTEuNzQyIDEwOC43MjQsOTEuNzQyIEMxMDguMDgzLDkxLjc0MiAxMDcuNDgxLDkxLjU5MyAxMDYuOTI1LDkxLjQwMSBMMTA2LjkyNSw5MS40MDEgWiIgaWQ9IkZpbGwtMTkiIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTEzLjA5Nyw5MC4yMyBDMTE4LjQ4MSw4Ny4xMjIgMTIyLjg0NSw3OS41OTQgMTIyLjg0NSw3My40MTYgQzEyMi44NDUsNzEuMzY1IDEyMi4zNjIsNjkuNzI0IDEyMS41MjIsNjguNTU2IEMxMTkuNzM4LDY3LjMwNCAxMTcuMTQ4LDY3LjM2MiAxMTQuMjY1LDY5LjAyNiBDMTA4Ljg4MSw3Mi4xMzQgMTA0LjUxNyw3OS42NjIgMTA0LjUxNyw4NS44NCBDMTA0LjUxNyw4Ny44OTEgMTA1LDg5LjUzMiAxMDUuODQsOTAuNyBDMTA3LjYyNCw5MS45NTIgMTEwLjIxNCw5MS44OTQgMTEzLjA5Nyw5MC4yMyIgaWQ9IkZpbGwtMjAiIGZpbGw9IiNGQUZBRkEiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTA4LjcyNCw5MS42MTQgTDEwOC43MjQsOTEuNjE0IEMxMDcuNTgyLDkxLjYxNCAxMDYuNTY2LDkxLjQwMSAxMDUuNzA1LDkwLjc5NyBDMTA1LjY4NCw5MC43ODMgMTA1LjY2NSw5MC44MTEgMTA1LjY1LDkwLjc5IEMxMDQuNzU2LDg5LjU0NiAxMDQuMjgzLDg3Ljg0MiAxMDQuMjgzLDg1LjgxNyBDMTA0LjI4Myw3OS41NzUgMTA4LjcwOSw3MS45NTMgMTE0LjE0OCw2OC44MTIgQzExNS43MjIsNjcuOTA0IDExNy4yMzIsNjcuNDQ5IDExOC42MzgsNjcuNDQ5IEMxMTkuNzgsNjcuNDQ5IDEyMC43OTYsNjcuNzU4IDEyMS42NTYsNjguMzYyIEMxMjEuNjc4LDY4LjM3NyAxMjEuNjk3LDY4LjM5NyAxMjEuNzEyLDY4LjQxOCBDMTIyLjYwNiw2OS42NjIgMTIzLjA3OSw3MS4zOSAxMjMuMDc5LDczLjQxNSBDMTIzLjA3OSw3OS42NTggMTE4LjY1Myw4Ny4xOTggMTEzLjIxNCw5MC4zMzggQzExMS42NCw5MS4yNDcgMTEwLjEzLDkxLjYxNCAxMDguNzI0LDkxLjYxNCBMMTA4LjcyNCw5MS42MTQgWiBNMTA2LjAwNiw5MC41MDUgQzEwNi43OCw5MS4wMzcgMTA3LjY5NCw5MS4yODEgMTA4LjcyNCw5MS4yODEgQzExMC4wNDcsOTEuMjgxIDExMS40NzgsOTAuODY4IDExMi45OCw5MC4wMDEgQzExOC4yOTEsODYuOTM1IDEyMi42MTEsNzkuNDk2IDEyMi42MTEsNzMuNDAzIEMxMjIuNjExLDcxLjQ5NCAxMjIuMTc3LDY5Ljg4IDEyMS4zNTYsNjguNzE4IEMxMjAuNTgyLDY4LjE4NSAxMTkuNjY4LDY3LjkxOSAxMTguNjM4LDY3LjkxOSBDMTE3LjMxNSw2Ny45MTkgMTE1Ljg4Myw2OC4zNiAxMTQuMzgyLDY5LjIyNyBDMTA5LjA3MSw3Mi4yOTMgMTA0Ljc1MSw3OS43MzMgMTA0Ljc1MSw4NS44MjYgQzEwNC43NTEsODcuNzM1IDEwNS4xODUsODkuMzQzIDEwNi4wMDYsOTAuNTA1IEwxMDYuMDA2LDkwLjUwNSBaIiBpZD0iRmlsbC0yMSIgZmlsbD0iIzYwN0Q4QiI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xNDkuMzE4LDcuMjYyIEwxMzkuMzM0LDE2LjE0IEwxNTUuMjI3LDI3LjE3MSBMMTYwLjgxNiwyMS4wNTkgTDE0OS4zMTgsNy4yNjIiIGlkPSJGaWxsLTIyIiBmaWxsPSIjRkFGQUZBIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTE2OS42NzYsMTMuODQgTDE1OS45MjgsMTkuNDY3IEMxNTYuMjg2LDIxLjU3IDE1MC40LDIxLjU4IDE0Ni43ODEsMTkuNDkxIEMxNDMuMTYxLDE3LjQwMiAxNDMuMTgsMTQuMDAzIDE0Ni44MjIsMTEuOSBMMTU2LjMxNyw2LjI5MiBMMTQ5LjU4OCwyLjQwNyBMNjcuNzUyLDQ5LjQ3OCBMMTEzLjY3NSw3NS45OTIgTDExNi43NTYsNzQuMjEzIEMxMTcuMzg3LDczLjg0OCAxMTcuNjI1LDczLjMxNSAxMTcuMzc0LDcyLjgyMyBDMTE1LjAxNyw2OC4xOTEgMTE0Ljc4MSw2My4yNzcgMTE2LjY5MSw1OC41NjEgQzEyMi4zMjksNDQuNjQxIDE0MS4yLDMzLjc0NiAxNjUuMzA5LDMwLjQ5MSBDMTczLjQ3OCwyOS4zODggMTgxLjk4OSwyOS41MjQgMTkwLjAxMywzMC44ODUgQzE5MC44NjUsMzEuMDMgMTkxLjc4OSwzMC44OTMgMTkyLjQyLDMwLjUyOCBMMTk1LjUwMSwyOC43NSBMMTY5LjY3NiwxMy44NCIgaWQ9IkZpbGwtMjMiIGZpbGw9IiNGQUZBRkEiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTEzLjY3NSw3Ni40NTkgQzExMy41OTQsNzYuNDU5IDExMy41MTQsNzYuNDM4IDExMy40NDIsNzYuMzk3IEw2Ny41MTgsNDkuODgyIEM2Ny4zNzQsNDkuNzk5IDY3LjI4NCw0OS42NDUgNjcuMjg1LDQ5LjQ3OCBDNjcuMjg1LDQ5LjMxMSA2Ny4zNzQsNDkuMTU3IDY3LjUxOSw0OS4wNzMgTDE0OS4zNTUsMi4wMDIgQzE0OS40OTksMS45MTkgMTQ5LjY3NywxLjkxOSAxNDkuODIxLDIuMDAyIEwxNTYuNTUsNS44ODcgQzE1Ni43NzQsNi4wMTcgMTU2Ljg1LDYuMzAyIDE1Ni43MjIsNi41MjYgQzE1Ni41OTIsNi43NDkgMTU2LjMwNyw2LjgyNiAxNTYuMDgzLDYuNjk2IEwxNDkuNTg3LDIuOTQ2IEw2OC42ODcsNDkuNDc5IEwxMTMuNjc1LDc1LjQ1MiBMMTE2LjUyMyw3My44MDggQzExNi43MTUsNzMuNjk3IDExNy4xNDMsNzMuMzk5IDExNi45NTgsNzMuMDM1IEMxMTQuNTQyLDY4LjI4NyAxMTQuMyw2My4yMjEgMTE2LjI1OCw1OC4zODUgQzExOS4wNjQsNTEuNDU4IDEyNS4xNDMsNDUuMTQzIDEzMy44NCw0MC4xMjIgQzE0Mi40OTcsMzUuMTI0IDE1My4zNTgsMzEuNjMzIDE2NS4yNDcsMzAuMDI4IEMxNzMuNDQ1LDI4LjkyMSAxODIuMDM3LDI5LjA1OCAxOTAuMDkxLDMwLjQyNSBDMTkwLjgzLDMwLjU1IDE5MS42NTIsMzAuNDMyIDE5Mi4xODYsMzAuMTI0IEwxOTQuNTY3LDI4Ljc1IEwxNjkuNDQyLDE0LjI0NCBDMTY5LjIxOSwxNC4xMTUgMTY5LjE0MiwxMy44MjkgMTY5LjI3MSwxMy42MDYgQzE2OS40LDEzLjM4MiAxNjkuNjg1LDEzLjMwNiAxNjkuOTA5LDEzLjQzNSBMMTk1LjczNCwyOC4zNDUgQzE5NS44NzksMjguNDI4IDE5NS45NjgsMjguNTgzIDE5NS45NjgsMjguNzUgQzE5NS45NjgsMjguOTE2IDE5NS44NzksMjkuMDcxIDE5NS43MzQsMjkuMTU0IEwxOTIuNjUzLDMwLjkzMyBDMTkxLjkzMiwzMS4zNSAxOTAuODksMzEuNTA4IDE4OS45MzUsMzEuMzQ2IEMxODEuOTcyLDI5Ljk5NSAxNzMuNDc4LDI5Ljg2IDE2NS4zNzIsMzAuOTU0IEMxNTMuNjAyLDMyLjU0MyAxNDIuODYsMzUuOTkzIDEzNC4zMDcsNDAuOTMxIEMxMjUuNzkzLDQ1Ljg0NyAxMTkuODUxLDUyLjAwNCAxMTcuMTI0LDU4LjczNiBDMTE1LjI3LDYzLjMxNCAxMTUuNTAxLDY4LjExMiAxMTcuNzksNzIuNjExIEMxMTguMTYsNzMuMzM2IDExNy44NDUsNzQuMTI0IDExNi45OSw3NC42MTcgTDExMy45MDksNzYuMzk3IEMxMTMuODM2LDc2LjQzOCAxMTMuNzU2LDc2LjQ1OSAxMTMuNjc1LDc2LjQ1OSIgaWQ9IkZpbGwtMjQiIGZpbGw9IiM0NTVBNjQiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTUzLjMxNiwyMS4yNzkgQzE1MC45MDMsMjEuMjc5IDE0OC40OTUsMjAuNzUxIDE0Ni42NjQsMTkuNjkzIEMxNDQuODQ2LDE4LjY0NCAxNDMuODQ0LDE3LjIzMiAxNDMuODQ0LDE1LjcxOCBDMTQzLjg0NCwxNC4xOTEgMTQ0Ljg2LDEyLjc2MyAxNDYuNzA1LDExLjY5OCBMMTU2LjE5OCw2LjA5MSBDMTU2LjMwOSw2LjAyNSAxNTYuNDUyLDYuMDYyIDE1Ni41MTgsNi4xNzMgQzE1Ni41ODMsNi4yODQgMTU2LjU0Nyw2LjQyNyAxNTYuNDM2LDYuNDkzIEwxNDYuOTQsMTIuMTAyIEMxNDUuMjQ0LDEzLjA4MSAxNDQuMzEyLDE0LjM2NSAxNDQuMzEyLDE1LjcxOCBDMTQ0LjMxMiwxNy4wNTggMTQ1LjIzLDE4LjMyNiAxNDYuODk3LDE5LjI4OSBDMTUwLjQ0NiwyMS4zMzggMTU2LjI0LDIxLjMyNyAxNTkuODExLDE5LjI2NSBMMTY5LjU1OSwxMy42MzcgQzE2OS42NywxMy41NzMgMTY5LjgxMywxMy42MTEgMTY5Ljg3OCwxMy43MjMgQzE2OS45NDMsMTMuODM0IDE2OS45MDQsMTMuOTc3IDE2OS43OTMsMTQuMDQyIEwxNjAuMDQ1LDE5LjY3IEMxNTguMTg3LDIwLjc0MiAxNTUuNzQ5LDIxLjI3OSAxNTMuMzE2LDIxLjI3OSIgaWQ9IkZpbGwtMjUiIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTEzLjY3NSw3NS45OTIgTDY3Ljc2Miw0OS40ODQiIGlkPSJGaWxsLTI2IiBmaWxsPSIjNDU1QTY0Ij48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTExMy42NzUsNzYuMzQyIEMxMTMuNjE1LDc2LjM0MiAxMTMuNTU1LDc2LjMyNyAxMTMuNSw3Ni4yOTUgTDY3LjU4Nyw0OS43ODcgQzY3LjQxOSw0OS42OSA2Ny4zNjIsNDkuNDc2IDY3LjQ1OSw0OS4zMDkgQzY3LjU1Niw0OS4xNDEgNjcuNzcsNDkuMDgzIDY3LjkzNyw0OS4xOCBMMTEzLjg1LDc1LjY4OCBDMTE0LjAxOCw3NS43ODUgMTE0LjA3NSw3NiAxMTMuOTc4LDc2LjE2NyBDMTEzLjkxNCw3Ni4yNzkgMTEzLjc5Niw3Ni4zNDIgMTEzLjY3NSw3Ni4zNDIiIGlkPSJGaWxsLTI3IiBmaWxsPSIjNDU1QTY0Ij48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTY3Ljc2Miw0OS40ODQgTDY3Ljc2MiwxMDMuNDg1IEM2Ny43NjIsMTA0LjU3NSA2OC41MzIsMTA1LjkwMyA2OS40ODIsMTA2LjQ1MiBMMTExLjk1NSwxMzAuOTczIEMxMTIuOTA1LDEzMS41MjIgMTEzLjY3NSwxMzEuMDgzIDExMy42NzUsMTI5Ljk5MyBMMTEzLjY3NSw3NS45OTIiIGlkPSJGaWxsLTI4IiBmaWxsPSIjRkFGQUZBIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTExMi43MjcsMTMxLjU2MSBDMTEyLjQzLDEzMS41NjEgMTEyLjEwNywxMzEuNDY2IDExMS43OCwxMzEuMjc2IEw2OS4zMDcsMTA2Ljc1NSBDNjguMjQ0LDEwNi4xNDIgNjcuNDEyLDEwNC43MDUgNjcuNDEyLDEwMy40ODUgTDY3LjQxMiw0OS40ODQgQzY3LjQxMiw0OS4yOSA2Ny41NjksNDkuMTM0IDY3Ljc2Miw0OS4xMzQgQzY3Ljk1Niw0OS4xMzQgNjguMTEzLDQ5LjI5IDY4LjExMyw0OS40ODQgTDY4LjExMywxMDMuNDg1IEM2OC4xMTMsMTA0LjQ0NSA2OC44MiwxMDUuNjY1IDY5LjY1NywxMDYuMTQ4IEwxMTIuMTMsMTMwLjY3IEMxMTIuNDc0LDEzMC44NjggMTEyLjc5MSwxMzAuOTEzIDExMywxMzAuNzkyIEMxMTMuMjA2LDEzMC42NzMgMTEzLjMyNSwxMzAuMzgxIDExMy4zMjUsMTI5Ljk5MyBMMTEzLjMyNSw3NS45OTIgQzExMy4zMjUsNzUuNzk4IDExMy40ODIsNzUuNjQxIDExMy42NzUsNzUuNjQxIEMxMTMuODY5LDc1LjY0MSAxMTQuMDI1LDc1Ljc5OCAxMTQuMDI1LDc1Ljk5MiBMMTE0LjAyNSwxMjkuOTkzIEMxMTQuMDI1LDEzMC42NDggMTEzLjc4NiwxMzEuMTQ3IDExMy4zNSwxMzEuMzk5IEMxMTMuMTYyLDEzMS41MDcgMTEyLjk1MiwxMzEuNTYxIDExMi43MjcsMTMxLjU2MSIgaWQ9IkZpbGwtMjkiIGZpbGw9IiM0NTVBNjQiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTEyLjg2LDQwLjUxMiBDMTEyLjg2LDQwLjUxMiAxMTIuODYsNDAuNTEyIDExMi44NTksNDAuNTEyIEMxMTAuNTQxLDQwLjUxMiAxMDguMzYsMzkuOTkgMTA2LjcxNywzOS4wNDEgQzEwNS4wMTIsMzguMDU3IDEwNC4wNzQsMzYuNzI2IDEwNC4wNzQsMzUuMjkyIEMxMDQuMDc0LDMzLjg0NyAxMDUuMDI2LDMyLjUwMSAxMDYuNzU0LDMxLjUwNCBMMTE4Ljc5NSwyNC41NTEgQzEyMC40NjMsMjMuNTg5IDEyMi42NjksMjMuMDU4IDEyNS4wMDcsMjMuMDU4IEMxMjcuMzI1LDIzLjA1OCAxMjkuNTA2LDIzLjU4MSAxMzEuMTUsMjQuNTMgQzEzMi44NTQsMjUuNTE0IDEzMy43OTMsMjYuODQ1IDEzMy43OTMsMjguMjc4IEMxMzMuNzkzLDI5LjcyNCAxMzIuODQxLDMxLjA2OSAxMzEuMTEzLDMyLjA2NyBMMTE5LjA3MSwzOS4wMTkgQzExNy40MDMsMzkuOTgyIDExNS4xOTcsNDAuNTEyIDExMi44Niw0MC41MTIgTDExMi44Niw0MC41MTIgWiBNMTI1LjAwNywyMy43NTkgQzEyMi43OSwyMy43NTkgMTIwLjcwOSwyNC4yNTYgMTE5LjE0NiwyNS4xNTggTDEwNy4xMDQsMzIuMTEgQzEwNS42MDIsMzIuOTc4IDEwNC43NzQsMzQuMTA4IDEwNC43NzQsMzUuMjkyIEMxMDQuNzc0LDM2LjQ2NSAxMDUuNTg5LDM3LjU4MSAxMDcuMDY3LDM4LjQzNCBDMTA4LjYwNSwzOS4zMjMgMTEwLjY2MywzOS44MTIgMTEyLjg1OSwzOS44MTIgTDExMi44NiwzOS44MTIgQzExNS4wNzYsMzkuODEyIDExNy4xNTgsMzkuMzE1IDExOC43MjEsMzguNDEzIEwxMzAuNzYyLDMxLjQ2IEMxMzIuMjY0LDMwLjU5MyAxMzMuMDkyLDI5LjQ2MyAxMzMuMDkyLDI4LjI3OCBDMTMzLjA5MiwyNy4xMDYgMTMyLjI3OCwyNS45OSAxMzAuOCwyNS4xMzYgQzEyOS4yNjEsMjQuMjQ4IDEyNy4yMDQsMjMuNzU5IDEyNS4wMDcsMjMuNzU5IEwxMjUuMDA3LDIzLjc1OSBaIiBpZD0iRmlsbC0zMCIgZmlsbD0iIzYwN0Q4QiI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xNjUuNjMsMTYuMjE5IEwxNTkuODk2LDE5LjUzIEMxNTYuNzI5LDIxLjM1OCAxNTEuNjEsMjEuMzY3IDE0OC40NjMsMTkuNTUgQzE0NS4zMTYsMTcuNzMzIDE0NS4zMzIsMTQuNzc4IDE0OC40OTksMTIuOTQ5IEwxNTQuMjMzLDkuNjM5IEwxNjUuNjMsMTYuMjE5IiBpZD0iRmlsbC0zMSIgZmlsbD0iI0ZBRkFGQSI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xNTQuMjMzLDEwLjQ0OCBMMTY0LjIyOCwxNi4yMTkgTDE1OS41NDYsMTguOTIzIEMxNTguMTEyLDE5Ljc1IDE1Ni4xOTQsMjAuMjA2IDE1NC4xNDcsMjAuMjA2IEMxNTIuMTE4LDIwLjIwNiAxNTAuMjI0LDE5Ljc1NyAxNDguODE0LDE4Ljk0MyBDMTQ3LjUyNCwxOC4xOTkgMTQ2LjgxNCwxNy4yNDkgMTQ2LjgxNCwxNi4yNjkgQzE0Ni44MTQsMTUuMjc4IDE0Ny41MzcsMTQuMzE0IDE0OC44NSwxMy41NTYgTDE1NC4yMzMsMTAuNDQ4IE0xNTQuMjMzLDkuNjM5IEwxNDguNDk5LDEyLjk0OSBDMTQ1LjMzMiwxNC43NzggMTQ1LjMxNiwxNy43MzMgMTQ4LjQ2MywxOS41NSBDMTUwLjAzMSwyMC40NTUgMTUyLjA4NiwyMC45MDcgMTU0LjE0NywyMC45MDcgQzE1Ni4yMjQsMjAuOTA3IDE1OC4zMDYsMjAuNDQ3IDE1OS44OTYsMTkuNTMgTDE2NS42MywxNi4yMTkgTDE1NC4yMzMsOS42MzkiIGlkPSJGaWxsLTMyIiBmaWxsPSIjNjA3RDhCIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTE0NS40NDUsNzIuNjY3IEwxNDUuNDQ1LDcyLjY2NyBDMTQzLjY3Miw3Mi42NjcgMTQyLjIwNCw3MS44MTcgMTQxLjIwMiw3MC40MjIgQzE0MS4xMzUsNzAuMzMgMTQxLjE0NSw3MC4xNDcgMTQxLjIyNSw3MC4wNjYgQzE0MS4zMDUsNjkuOTg1IDE0MS40MzIsNjkuOTQ2IDE0MS41MjUsNzAuMDExIEMxNDIuMzA2LDcwLjU1OSAxNDMuMjMxLDcwLjgyMyAxNDQuMjc2LDcwLjgyMiBDMTQ1LjU5OCw3MC44MjIgMTQ3LjAzLDcwLjM3NiAxNDguNTMyLDY5LjUwOSBDMTUzLjg0Miw2Ni40NDMgMTU4LjE2Myw1OC45ODcgMTU4LjE2Myw1Mi44OTQgQzE1OC4xNjMsNTAuOTY3IDE1Ny43MjEsNDkuMzMyIDE1Ni44ODQsNDguMTY4IEMxNTYuODE4LDQ4LjA3NiAxNTYuODI4LDQ3Ljk0OCAxNTYuOTA4LDQ3Ljg2NyBDMTU2Ljk4OCw0Ny43ODYgMTU3LjExNCw0Ny43NzQgMTU3LjIwOCw0Ny44NCBDMTU4Ljg3OCw0OS4wMTIgMTU5Ljc5OCw1MS4yMiAxNTkuNzk4LDU0LjA1OSBDMTU5Ljc5OCw2MC4zMDEgMTU1LjM3Myw2OC4wNDYgMTQ5LjkzMyw3MS4xODYgQzE0OC4zNiw3Mi4wOTQgMTQ2Ljg1LDcyLjY2NyAxNDUuNDQ1LDcyLjY2NyBMMTQ1LjQ0NSw3Mi42NjcgWiBNMTQyLjQ3Niw3MSBDMTQzLjI5LDcxLjY1MSAxNDQuMjk2LDcyLjAwMiAxNDUuNDQ1LDcyLjAwMiBDMTQ2Ljc2Nyw3Mi4wMDIgMTQ4LjE5OCw3MS41NSAxNDkuNyw3MC42ODIgQzE1NS4wMSw2Ny42MTcgMTU5LjMzMSw2MC4xNTkgMTU5LjMzMSw1NC4wNjUgQzE1OS4zMzEsNTIuMDg1IDE1OC44NjgsNTAuNDM1IDE1OC4wMDYsNDkuMjcyIEMxNTguNDE3LDUwLjMwNyAxNTguNjMsNTEuNTMyIDE1OC42Myw1Mi44OTIgQzE1OC42Myw1OS4xMzQgMTU0LjIwNSw2Ni43NjcgMTQ4Ljc2NSw2OS45MDcgQzE0Ny4xOTIsNzAuODE2IDE0NS42ODEsNzEuMjgzIDE0NC4yNzYsNzEuMjgzIEMxNDMuNjM0LDcxLjI4MyAxNDMuMDMzLDcxLjE5MiAxNDIuNDc2LDcxIEwxNDIuNDc2LDcxIFoiIGlkPSJGaWxsLTMzIiBmaWxsPSIjNjA3RDhCIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTE0OC42NDgsNjkuNzA0IEMxNTQuMDMyLDY2LjU5NiAxNTguMzk2LDU5LjA2OCAxNTguMzk2LDUyLjg5MSBDMTU4LjM5Niw1MC44MzkgMTU3LjkxMyw0OS4xOTggMTU3LjA3NCw0OC4wMyBDMTU1LjI4OSw0Ni43NzggMTUyLjY5OSw0Ni44MzYgMTQ5LjgxNiw0OC41MDEgQzE0NC40MzMsNTEuNjA5IDE0MC4wNjgsNTkuMTM3IDE0MC4wNjgsNjUuMzE0IEMxNDAuMDY4LDY3LjM2NSAxNDAuNTUyLDY5LjAwNiAxNDEuMzkxLDcwLjE3NCBDMTQzLjE3Niw3MS40MjcgMTQ1Ljc2NSw3MS4zNjkgMTQ4LjY0OCw2OS43MDQiIGlkPSJGaWxsLTM0IiBmaWxsPSIjRkFGQUZBIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTE0NC4yNzYsNzEuMjc2IEwxNDQuMjc2LDcxLjI3NiBDMTQzLjEzMyw3MS4yNzYgMTQyLjExOCw3MC45NjkgMTQxLjI1Nyw3MC4zNjUgQzE0MS4yMzYsNzAuMzUxIDE0MS4yMTcsNzAuMzMyIDE0MS4yMDIsNzAuMzExIEMxNDAuMzA3LDY5LjA2NyAxMzkuODM1LDY3LjMzOSAxMzkuODM1LDY1LjMxNCBDMTM5LjgzNSw1OS4wNzMgMTQ0LjI2LDUxLjQzOSAxNDkuNyw0OC4yOTggQzE1MS4yNzMsNDcuMzkgMTUyLjc4NCw0Ni45MjkgMTU0LjE4OSw0Ni45MjkgQzE1NS4zMzIsNDYuOTI5IDE1Ni4zNDcsNDcuMjM2IDE1Ny4yMDgsNDcuODM5IEMxNTcuMjI5LDQ3Ljg1NCAxNTcuMjQ4LDQ3Ljg3MyAxNTcuMjYzLDQ3Ljg5NCBDMTU4LjE1Nyw0OS4xMzggMTU4LjYzLDUwLjg2NSAxNTguNjMsNTIuODkxIEMxNTguNjMsNTkuMTMyIDE1NC4yMDUsNjYuNzY2IDE0OC43NjUsNjkuOTA3IEMxNDcuMTkyLDcwLjgxNSAxNDUuNjgxLDcxLjI3NiAxNDQuMjc2LDcxLjI3NiBMMTQ0LjI3Niw3MS4yNzYgWiBNMTQxLjU1OCw3MC4xMDQgQzE0Mi4zMzEsNzAuNjM3IDE0My4yNDUsNzEuMDA1IDE0NC4yNzYsNzEuMDA1IEMxNDUuNTk4LDcxLjAwNSAxNDcuMDMsNzAuNDY3IDE0OC41MzIsNjkuNiBDMTUzLjg0Miw2Ni41MzQgMTU4LjE2Myw1OS4wMzMgMTU4LjE2Myw1Mi45MzkgQzE1OC4xNjMsNTEuMDMxIDE1Ny43MjksNDkuMzg1IDE1Ni45MDcsNDguMjIzIEMxNTYuMTMzLDQ3LjY5MSAxNTUuMjE5LDQ3LjQwOSAxNTQuMTg5LDQ3LjQwOSBDMTUyLjg2Nyw0Ny40MDkgMTUxLjQzNSw0Ny44NDIgMTQ5LjkzMyw0OC43MDkgQzE0NC42MjMsNTEuNzc1IDE0MC4zMDIsNTkuMjczIDE0MC4zMDIsNjUuMzY2IEMxNDAuMzAyLDY3LjI3NiAxNDAuNzM2LDY4Ljk0MiAxNDEuNTU4LDcwLjEwNCBMMTQxLjU1OCw3MC4xMDQgWiIgaWQ9IkZpbGwtMzUiIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTUwLjcyLDY1LjM2MSBMMTUwLjM1Nyw2NS4wNjYgQzE1MS4xNDcsNjQuMDkyIDE1MS44NjksNjMuMDQgMTUyLjUwNSw2MS45MzggQzE1My4zMTMsNjAuNTM5IDE1My45NzgsNTkuMDY3IDE1NC40ODIsNTcuNTYzIEwxNTQuOTI1LDU3LjcxMiBDMTU0LjQxMiw1OS4yNDUgMTUzLjczMyw2MC43NDUgMTUyLjkxLDYyLjE3MiBDMTUyLjI2Miw2My4yOTUgMTUxLjUyNSw2NC4zNjggMTUwLjcyLDY1LjM2MSIgaWQ9IkZpbGwtMzYiIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTE1LjkxNyw4NC41MTQgTDExNS41NTQsODQuMjIgQzExNi4zNDQsODMuMjQ1IDExNy4wNjYsODIuMTk0IDExNy43MDIsODEuMDkyIEMxMTguNTEsNzkuNjkyIDExOS4xNzUsNzguMjIgMTE5LjY3OCw3Ni43MTcgTDEyMC4xMjEsNzYuODY1IEMxMTkuNjA4LDc4LjM5OCAxMTguOTMsNzkuODk5IDExOC4xMDYsODEuMzI2IEMxMTcuNDU4LDgyLjQ0OCAxMTYuNzIyLDgzLjUyMSAxMTUuOTE3LDg0LjUxNCIgaWQ9IkZpbGwtMzciIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTE0LDEzMC40NzYgTDExNCwxMzAuMDA4IEwxMTQsNzYuMDUyIEwxMTQsNzUuNTg0IEwxMTQsNzYuMDUyIEwxMTQsMTMwLjAwOCBMMTE0LDEzMC40NzYiIGlkPSJGaWxsLTM4IiBmaWxsPSIjNjA3RDhCIj48L3BhdGg+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICA8ZyBpZD0iSW1wb3J0ZWQtTGF5ZXJzLUNvcHkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDYyLjAwMDAwMCwgMC4wMDAwMDApIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTkuODIyLDM3LjQ3NCBDMTkuODM5LDM3LjMzOSAxOS43NDcsMzcuMTk0IDE5LjU1NSwzNy4wODIgQzE5LjIyOCwzNi44OTQgMTguNzI5LDM2Ljg3MiAxOC40NDYsMzcuMDM3IEwxMi40MzQsNDAuNTA4IEMxMi4zMDMsNDAuNTg0IDEyLjI0LDQwLjY4NiAxMi4yNDMsNDAuNzkzIEMxMi4yNDUsNDAuOTI1IDEyLjI0NSw0MS4yNTQgMTIuMjQ1LDQxLjM3MSBMMTIuMjQ1LDQxLjQxNCBMMTIuMjM4LDQxLjU0MiBDOC4xNDgsNDMuODg3IDUuNjQ3LDQ1LjMyMSA1LjY0Nyw0NS4zMjEgQzUuNjQ2LDQ1LjMyMSAzLjU3LDQ2LjM2NyAyLjg2LDUwLjUxMyBDMi44Niw1MC41MTMgMS45NDgsNTcuNDc0IDEuOTYyLDcwLjI1OCBDMS45NzcsODIuODI4IDIuNTY4LDg3LjMyOCAzLjEyOSw5MS42MDkgQzMuMzQ5LDkzLjI5MyA2LjEzLDkzLjczNCA2LjEzLDkzLjczNCBDNi40NjEsOTMuNzc0IDYuODI4LDkzLjcwNyA3LjIxLDkzLjQ4NiBMODIuNDgzLDQ5LjkzNSBDODQuMjkxLDQ4Ljg2NiA4NS4xNSw0Ni4yMTYgODUuNTM5LDQzLjY1MSBDODYuNzUyLDM1LjY2MSA4Ny4yMTQsMTAuNjczIDg1LjI2NCwzLjc3MyBDODUuMDY4LDMuMDggODQuNzU0LDIuNjkgODQuMzk2LDIuNDkxIEw4Mi4zMSwxLjcwMSBDODEuNTgzLDEuNzI5IDgwLjg5NCwyLjE2OCA4MC43NzYsMi4yMzYgQzgwLjYzNiwyLjMxNyA0MS44MDcsMjQuNTg1IDIwLjAzMiwzNy4wNzIgTDE5LjgyMiwzNy40NzQiIGlkPSJGaWxsLTEiIGZpbGw9IiNGRkZGRkYiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNODIuMzExLDEuNzAxIEw4NC4zOTYsMi40OTEgQzg0Ljc1NCwyLjY5IDg1LjA2OCwzLjA4IDg1LjI2NCwzLjc3MyBDODcuMjEzLDEwLjY3MyA4Ni43NTEsMzUuNjYgODUuNTM5LDQzLjY1MSBDODUuMTQ5LDQ2LjIxNiA4NC4yOSw0OC44NjYgODIuNDgzLDQ5LjkzNSBMNy4yMSw5My40ODYgQzYuODk3LDkzLjY2NyA2LjU5NSw5My43NDQgNi4zMTQsOTMuNzQ0IEw2LjEzMSw5My43MzMgQzYuMTMxLDkzLjczNCAzLjM0OSw5My4yOTMgMy4xMjgsOTEuNjA5IEMyLjU2OCw4Ny4zMjcgMS45NzcsODIuODI4IDEuOTYzLDcwLjI1OCBDMS45NDgsNTcuNDc0IDIuODYsNTAuNTEzIDIuODYsNTAuNTEzIEMzLjU3LDQ2LjM2NyA1LjY0Nyw0NS4zMjEgNS42NDcsNDUuMzIxIEM1LjY0Nyw0NS4zMjEgOC4xNDgsNDMuODg3IDEyLjIzOCw0MS41NDIgTDEyLjI0NSw0MS40MTQgTDEyLjI0NSw0MS4zNzEgQzEyLjI0NSw0MS4yNTQgMTIuMjQ1LDQwLjkyNSAxMi4yNDMsNDAuNzkzIEMxMi4yNCw0MC42ODYgMTIuMzAyLDQwLjU4MyAxMi40MzQsNDAuNTA4IEwxOC40NDYsMzcuMDM2IEMxOC41NzQsMzYuOTYyIDE4Ljc0NiwzNi45MjYgMTguOTI3LDM2LjkyNiBDMTkuMTQ1LDM2LjkyNiAxOS4zNzYsMzYuOTc5IDE5LjU1NCwzNy4wODIgQzE5Ljc0NywzNy4xOTQgMTkuODM5LDM3LjM0IDE5LjgyMiwzNy40NzQgTDIwLjAzMywzNy4wNzIgQzQxLjgwNiwyNC41ODUgODAuNjM2LDIuMzE4IDgwLjc3NywyLjIzNiBDODAuODk0LDIuMTY4IDgxLjU4MywxLjcyOSA4Mi4zMTEsMS43MDEgTTgyLjMxMSwwLjcwNCBMODIuMjcyLDAuNzA1IEM4MS42NTQsMC43MjggODAuOTg5LDAuOTQ5IDgwLjI5OCwxLjM2MSBMODAuMjc3LDEuMzczIEM4MC4xMjksMS40NTggNTkuNzY4LDEzLjEzNSAxOS43NTgsMzYuMDc5IEMxOS41LDM1Ljk4MSAxOS4yMTQsMzUuOTI5IDE4LjkyNywzNS45MjkgQzE4LjU2MiwzNS45MjkgMTguMjIzLDM2LjAxMyAxNy45NDcsMzYuMTczIEwxMS45MzUsMzkuNjQ0IEMxMS40OTMsMzkuODk5IDExLjIzNiw0MC4zMzQgMTEuMjQ2LDQwLjgxIEwxMS4yNDcsNDAuOTYgTDUuMTY3LDQ0LjQ0NyBDNC43OTQsNDQuNjQ2IDIuNjI1LDQ1Ljk3OCAxLjg3Nyw1MC4zNDUgTDEuODcxLDUwLjM4NCBDMS44NjIsNTAuNDU0IDAuOTUxLDU3LjU1NyAwLjk2NSw3MC4yNTkgQzAuOTc5LDgyLjg3OSAxLjU2OCw4Ny4zNzUgMi4xMzcsOTEuNzI0IEwyLjEzOSw5MS43MzkgQzIuNDQ3LDk0LjA5NCA1LjYxNCw5NC42NjIgNS45NzUsOTQuNzE5IEw2LjAwOSw5NC43MjMgQzYuMTEsOTQuNzM2IDYuMjEzLDk0Ljc0MiA2LjMxNCw5NC43NDIgQzYuNzksOTQuNzQyIDcuMjYsOTQuNjEgNy43MSw5NC4zNSBMODIuOTgzLDUwLjc5OCBDODQuNzk0LDQ5LjcyNyA4NS45ODIsNDcuMzc1IDg2LjUyNSw0My44MDEgQzg3LjcxMSwzNS45ODcgODguMjU5LDEwLjcwNSA4Ni4yMjQsMy41MDIgQzg1Ljk3MSwyLjYwOSA4NS41MiwxLjk3NSA4NC44ODEsMS42MiBMODQuNzQ5LDEuNTU4IEw4Mi42NjQsMC43NjkgQzgyLjU1MSwwLjcyNSA4Mi40MzEsMC43MDQgODIuMzExLDAuNzA0IiBpZD0iRmlsbC0yIiBmaWxsPSIjNDU1QTY0Ij48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTY2LjI2NywxMS41NjUgTDY3Ljc2MiwxMS45OTkgTDExLjQyMyw0NC4zMjUiIGlkPSJGaWxsLTMiIGZpbGw9IiNGRkZGRkYiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTIuMjAyLDkwLjU0NSBDMTIuMDI5LDkwLjU0NSAxMS44NjIsOTAuNDU1IDExLjc2OSw5MC4yOTUgQzExLjYzMiw5MC4wNTcgMTEuNzEzLDg5Ljc1MiAxMS45NTIsODkuNjE0IEwzMC4zODksNzguOTY5IEMzMC42MjgsNzguODMxIDMwLjkzMyw3OC45MTMgMzEuMDcxLDc5LjE1MiBDMzEuMjA4LDc5LjM5IDMxLjEyNyw3OS42OTYgMzAuODg4LDc5LjgzMyBMMTIuNDUxLDkwLjQ3OCBMMTIuMjAyLDkwLjU0NSIgaWQ9IkZpbGwtNCIgZmlsbD0iIzYwN0Q4QiI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xMy43NjQsNDIuNjU0IEwxMy42NTYsNDIuNTkyIEwxMy43MDIsNDIuNDIxIEwxOC44MzcsMzkuNDU3IEwxOS4wMDcsMzkuNTAyIEwxOC45NjIsMzkuNjczIEwxMy44MjcsNDIuNjM3IEwxMy43NjQsNDIuNjU0IiBpZD0iRmlsbC01IiBmaWxsPSIjNjA3RDhCIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTguNTIsOTAuMzc1IEw4LjUyLDQ2LjQyMSBMOC41ODMsNDYuMzg1IEw3NS44NCw3LjU1NCBMNzUuODQsNTEuNTA4IEw3NS43NzgsNTEuNTQ0IEw4LjUyLDkwLjM3NSBMOC41Miw5MC4zNzUgWiBNOC43Nyw0Ni41NjQgTDguNzcsODkuOTQ0IEw3NS41OTEsNTEuMzY1IEw3NS41OTEsNy45ODUgTDguNzcsNDYuNTY0IEw4Ljc3LDQ2LjU2NCBaIiBpZD0iRmlsbC02IiBmaWxsPSIjNjA3RDhCIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTI0Ljk4Niw4My4xODIgQzI0Ljc1Niw4My4zMzEgMjQuMzc0LDgzLjU2NiAyNC4xMzcsODMuNzA1IEwxMi42MzIsOTAuNDA2IEMxMi4zOTUsOTAuNTQ1IDEyLjQyNiw5MC42NTggMTIuNyw5MC42NTggTDEzLjI2NSw5MC42NTggQzEzLjU0LDkwLjY1OCAxMy45NTgsOTAuNTQ1IDE0LjE5NSw5MC40MDYgTDI1LjcsODMuNzA1IEMyNS45MzcsODMuNTY2IDI2LjEyOCw4My40NTIgMjYuMTI1LDgzLjQ0OSBDMjYuMTIyLDgzLjQ0NyAyNi4xMTksODMuMjIgMjYuMTE5LDgyLjk0NiBDMjYuMTE5LDgyLjY3MiAyNS45MzEsODIuNTY5IDI1LjcwMSw4Mi43MTkgTDI0Ljk4Niw4My4xODIiIGlkPSJGaWxsLTciIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTMuMjY2LDkwLjc4MiBMMTIuNyw5MC43ODIgQzEyLjUsOTAuNzgyIDEyLjM4NCw5MC43MjYgMTIuMzU0LDkwLjYxNiBDMTIuMzI0LDkwLjUwNiAxMi4zOTcsOTAuMzk5IDEyLjU2OSw5MC4yOTkgTDI0LjA3NCw4My41OTcgQzI0LjMxLDgzLjQ1OSAyNC42ODksODMuMjI2IDI0LjkxOCw4My4wNzggTDI1LjYzMyw4Mi42MTQgQzI1LjcyMyw4Mi41NTUgMjUuODEzLDgyLjUyNSAyNS44OTksODIuNTI1IEMyNi4wNzEsODIuNTI1IDI2LjI0NCw4Mi42NTUgMjYuMjQ0LDgyLjk0NiBDMjYuMjQ0LDgzLjE2IDI2LjI0NSw4My4zMDkgMjYuMjQ3LDgzLjM4MyBMMjYuMjUzLDgzLjM4NyBMMjYuMjQ5LDgzLjQ1NiBDMjYuMjQ2LDgzLjUzMSAyNi4yNDYsODMuNTMxIDI1Ljc2Myw4My44MTIgTDE0LjI1OCw5MC41MTQgQzE0LDkwLjY2NSAxMy41NjQsOTAuNzgyIDEzLjI2Niw5MC43ODIgTDEzLjI2Niw5MC43ODIgWiBNMTIuNjY2LDkwLjUzMiBMMTIuNyw5MC41MzMgTDEzLjI2Niw5MC41MzMgQzEzLjUxOCw5MC41MzMgMTMuOTE1LDkwLjQyNSAxNC4xMzIsOTAuMjk5IEwyNS42MzcsODMuNTk3IEMyNS44MDUsODMuNDk5IDI1LjkzMSw4My40MjQgMjUuOTk4LDgzLjM4MyBDMjUuOTk0LDgzLjI5OSAyNS45OTQsODMuMTY1IDI1Ljk5NCw4Mi45NDYgTDI1Ljg5OSw4Mi43NzUgTDI1Ljc2OCw4Mi44MjQgTDI1LjA1NCw4My4yODcgQzI0LjgyMiw4My40MzcgMjQuNDM4LDgzLjY3MyAyNC4yLDgzLjgxMiBMMTIuNjk1LDkwLjUxNCBMMTIuNjY2LDkwLjUzMiBMMTIuNjY2LDkwLjUzMiBaIiBpZD0iRmlsbC04IiBmaWxsPSIjNjA3RDhCIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTEzLjI2Niw4OS44NzEgTDEyLjcsODkuODcxIEMxMi41LDg5Ljg3MSAxMi4zODQsODkuODE1IDEyLjM1NCw4OS43MDUgQzEyLjMyNCw4OS41OTUgMTIuMzk3LDg5LjQ4OCAxMi41NjksODkuMzg4IEwyNC4wNzQsODIuNjg2IEMyNC4zMzIsODIuNTM1IDI0Ljc2OCw4Mi40MTggMjUuMDY3LDgyLjQxOCBMMjUuNjMyLDgyLjQxOCBDMjUuODMyLDgyLjQxOCAyNS45NDgsODIuNDc0IDI1Ljk3OCw4Mi41ODQgQzI2LjAwOCw4Mi42OTQgMjUuOTM1LDgyLjgwMSAyNS43NjMsODIuOTAxIEwxNC4yNTgsODkuNjAzIEMxNCw4OS43NTQgMTMuNTY0LDg5Ljg3MSAxMy4yNjYsODkuODcxIEwxMy4yNjYsODkuODcxIFogTTEyLjY2Niw4OS42MjEgTDEyLjcsODkuNjIyIEwxMy4yNjYsODkuNjIyIEMxMy41MTgsODkuNjIyIDEzLjkxNSw4OS41MTUgMTQuMTMyLDg5LjM4OCBMMjUuNjM3LDgyLjY4NiBMMjUuNjY3LDgyLjY2OCBMMjUuNjMyLDgyLjY2NyBMMjUuMDY3LDgyLjY2NyBDMjQuODE1LDgyLjY2NyAyNC40MTgsODIuNzc1IDI0LjIsODIuOTAxIEwxMi42OTUsODkuNjAzIEwxMi42NjYsODkuNjIxIEwxMi42NjYsODkuNjIxIFoiIGlkPSJGaWxsLTkiIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNMTIuMzcsOTAuODAxIEwxMi4zNyw4OS41NTQgTDEyLjM3LDkwLjgwMSIgaWQ9IkZpbGwtMTAiIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNNi4xMyw5My45MDEgQzUuMzc5LDkzLjgwOCA0LjgxNiw5My4xNjQgNC42OTEsOTIuNTI1IEMzLjg2LDg4LjI4NyAzLjU0LDgzLjc0MyAzLjUyNiw3MS4xNzMgQzMuNTExLDU4LjM4OSA0LjQyMyw1MS40MjggNC40MjMsNTEuNDI4IEM1LjEzNCw0Ny4yODIgNy4yMSw0Ni4yMzYgNy4yMSw0Ni4yMzYgQzcuMjEsNDYuMjM2IDgxLjY2NywzLjI1IDgyLjA2OSwzLjAxNyBDODIuMjkyLDIuODg4IDg0LjU1NiwxLjQzMyA4NS4yNjQsMy45NCBDODcuMjE0LDEwLjg0IDg2Ljc1MiwzNS44MjcgODUuNTM5LDQzLjgxOCBDODUuMTUsNDYuMzgzIDg0LjI5MSw0OS4wMzMgODIuNDgzLDUwLjEwMSBMNy4yMSw5My42NTMgQzYuODI4LDkzLjg3NCA2LjQ2MSw5My45NDEgNi4xMyw5My45MDEgQzYuMTMsOTMuOTAxIDMuMzQ5LDkzLjQ2IDMuMTI5LDkxLjc3NiBDMi41NjgsODcuNDk1IDEuOTc3LDgyLjk5NSAxLjk2Miw3MC40MjUgQzEuOTQ4LDU3LjY0MSAyLjg2LDUwLjY4IDIuODYsNTAuNjggQzMuNTcsNDYuNTM0IDUuNjQ3LDQ1LjQ4OSA1LjY0Nyw0NS40ODkgQzUuNjQ2LDQ1LjQ4OSA4LjA2NSw0NC4wOTIgMTIuMjQ1LDQxLjY3OSBMMTMuMTE2LDQxLjU2IEwxOS43MTUsMzcuNzMgTDE5Ljc2MSwzNy4yNjkgTDYuMTMsOTMuOTAxIiBpZD0iRmlsbC0xMSIgZmlsbD0iI0ZBRkFGQSI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik02LjMxNyw5NC4xNjEgTDYuMTAyLDk0LjE0OCBMNi4xMDEsOTQuMTQ4IEw1Ljg1Nyw5NC4xMDEgQzUuMTM4LDkzLjk0NSAzLjA4NSw5My4zNjUgMi44ODEsOTEuODA5IEMyLjMxMyw4Ny40NjkgMS43MjcsODIuOTk2IDEuNzEzLDcwLjQyNSBDMS42OTksNTcuNzcxIDIuNjA0LDUwLjcxOCAyLjYxMyw1MC42NDggQzMuMzM4LDQ2LjQxNyA1LjQ0NSw0NS4zMSA1LjUzNSw0NS4yNjYgTDEyLjE2Myw0MS40MzkgTDEzLjAzMyw0MS4zMiBMMTkuNDc5LDM3LjU3OCBMMTkuNTEzLDM3LjI0NCBDMTkuNTI2LDM3LjEwNyAxOS42NDcsMzcuMDA4IDE5Ljc4NiwzNy4wMjEgQzE5LjkyMiwzNy4wMzQgMjAuMDIzLDM3LjE1NiAyMC4wMDksMzcuMjkzIEwxOS45NSwzNy44ODIgTDEzLjE5OCw0MS44MDEgTDEyLjMyOCw0MS45MTkgTDUuNzcyLDQ1LjcwNCBDNS43NDEsNDUuNzIgMy43ODIsNDYuNzcyIDMuMTA2LDUwLjcyMiBDMy4wOTksNTAuNzgyIDIuMTk4LDU3LjgwOCAyLjIxMiw3MC40MjQgQzIuMjI2LDgyLjk2MyAyLjgwOSw4Ny40MiAzLjM3Myw5MS43MjkgQzMuNDY0LDkyLjQyIDQuMDYyLDkyLjg4MyA0LjY4Miw5My4xODEgQzQuNTY2LDkyLjk4NCA0LjQ4Niw5Mi43NzYgNC40NDYsOTIuNTcyIEMzLjY2NSw4OC41ODggMy4yOTEsODQuMzcgMy4yNzYsNzEuMTczIEMzLjI2Miw1OC41MiA0LjE2Nyw1MS40NjYgNC4xNzYsNTEuMzk2IEM0LjkwMSw0Ny4xNjUgNy4wMDgsNDYuMDU5IDcuMDk4LDQ2LjAxNCBDNy4wOTQsNDYuMDE1IDgxLjU0MiwzLjAzNCA4MS45NDQsMi44MDIgTDgxLjk3MiwyLjc4NSBDODIuODc2LDIuMjQ3IDgzLjY5MiwyLjA5NyA4NC4zMzIsMi4zNTIgQzg0Ljg4NywyLjU3MyA4NS4yODEsMy4wODUgODUuNTA0LDMuODcyIEM4Ny41MTgsMTEgODYuOTY0LDM2LjA5MSA4NS43ODUsNDMuODU1IEM4NS4yNzgsNDcuMTk2IDg0LjIxLDQ5LjM3IDgyLjYxLDUwLjMxNyBMNy4zMzUsOTMuODY5IEM2Ljk5OSw5NC4wNjMgNi42NTgsOTQuMTYxIDYuMzE3LDk0LjE2MSBMNi4zMTcsOTQuMTYxIFogTTYuMTcsOTMuNjU0IEM2LjQ2Myw5My42OSA2Ljc3NCw5My42MTcgNy4wODUsOTMuNDM3IEw4Mi4zNTgsNDkuODg2IEM4NC4xODEsNDguODA4IDg0Ljk2LDQ1Ljk3MSA4NS4yOTIsNDMuNzggQzg2LjQ2NiwzNi4wNDkgODcuMDIzLDExLjA4NSA4NS4wMjQsNC4wMDggQzg0Ljg0NiwzLjM3NyA4NC41NTEsMi45NzYgODQuMTQ4LDIuODE2IEM4My42NjQsMi42MjMgODIuOTgyLDIuNzY0IDgyLjIyNywzLjIxMyBMODIuMTkzLDMuMjM0IEM4MS43OTEsMy40NjYgNy4zMzUsNDYuNDUyIDcuMzM1LDQ2LjQ1MiBDNy4zMDQsNDYuNDY5IDUuMzQ2LDQ3LjUyMSA0LjY2OSw1MS40NzEgQzQuNjYyLDUxLjUzIDMuNzYxLDU4LjU1NiAzLjc3NSw3MS4xNzMgQzMuNzksODQuMzI4IDQuMTYxLDg4LjUyNCA0LjkzNiw5Mi40NzYgQzUuMDI2LDkyLjkzNyA1LjQxMiw5My40NTkgNS45NzMsOTMuNjE1IEM2LjA4Nyw5My42NCA2LjE1OCw5My42NTIgNi4xNjksOTMuNjU0IEw2LjE3LDkzLjY1NCBMNi4xNyw5My42NTQgWiIgaWQ9IkZpbGwtMTIiIGZpbGw9IiM0NTVBNjQiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNNy4zMTcsNjguOTgyIEM3LjgwNiw2OC43MDEgOC4yMDIsNjguOTI2IDguMjAyLDY5LjQ4NyBDOC4yMDIsNzAuMDQ3IDcuODA2LDcwLjczIDcuMzE3LDcxLjAxMiBDNi44MjksNzEuMjk0IDYuNDMzLDcxLjA2OSA2LjQzMyw3MC41MDggQzYuNDMzLDY5Ljk0OCA2LjgyOSw2OS4yNjUgNy4zMTcsNjguOTgyIiBpZD0iRmlsbC0xMyIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik02LjkyLDcxLjEzMyBDNi42MzEsNzEuMTMzIDYuNDMzLDcwLjkwNSA2LjQzMyw3MC41MDggQzYuNDMzLDY5Ljk0OCA2LjgyOSw2OS4yNjUgNy4zMTcsNjguOTgyIEM3LjQ2LDY4LjkgNy41OTUsNjguODYxIDcuNzE0LDY4Ljg2MSBDOC4wMDMsNjguODYxIDguMjAyLDY5LjA5IDguMjAyLDY5LjQ4NyBDOC4yMDIsNzAuMDQ3IDcuODA2LDcwLjczIDcuMzE3LDcxLjAxMiBDNy4xNzQsNzEuMDk0IDcuMDM5LDcxLjEzMyA2LjkyLDcxLjEzMyBNNy43MTQsNjguNjc0IEM3LjU1Nyw2OC42NzQgNy4zOTIsNjguNzIzIDcuMjI0LDY4LjgyMSBDNi42NzYsNjkuMTM4IDYuMjQ2LDY5Ljg3OSA2LjI0Niw3MC41MDggQzYuMjQ2LDcwLjk5NCA2LjUxNyw3MS4zMiA2LjkyLDcxLjMyIEM3LjA3OCw3MS4zMiA3LjI0Myw3MS4yNzEgNy40MTEsNzEuMTc0IEM3Ljk1OSw3MC44NTcgOC4zODksNzAuMTE3IDguMzg5LDY5LjQ4NyBDOC4zODksNjkuMDAxIDguMTE3LDY4LjY3NCA3LjcxNCw2OC42NzQiIGlkPSJGaWxsLTE0IiBmaWxsPSIjODA5N0EyIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTYuOTIsNzAuOTQ3IEM2LjY0OSw3MC45NDcgNi42MjEsNzAuNjQgNi42MjEsNzAuNTA4IEM2LjYyMSw3MC4wMTcgNi45ODIsNjkuMzkyIDcuNDExLDY5LjE0NSBDNy41MjEsNjkuMDgyIDcuNjI1LDY5LjA0OSA3LjcxNCw2OS4wNDkgQzcuOTg2LDY5LjA0OSA4LjAxNSw2OS4zNTUgOC4wMTUsNjkuNDg3IEM4LjAxNSw2OS45NzggNy42NTIsNzAuNjAzIDcuMjI0LDcwLjg1MSBDNy4xMTUsNzAuOTE0IDcuMDEsNzAuOTQ3IDYuOTIsNzAuOTQ3IE03LjcxNCw2OC44NjEgQzcuNTk1LDY4Ljg2MSA3LjQ2LDY4LjkgNy4zMTcsNjguOTgyIEM2LjgyOSw2OS4yNjUgNi40MzMsNjkuOTQ4IDYuNDMzLDcwLjUwOCBDNi40MzMsNzAuOTA1IDYuNjMxLDcxLjEzMyA2LjkyLDcxLjEzMyBDNy4wMzksNzEuMTMzIDcuMTc0LDcxLjA5NCA3LjMxNyw3MS4wMTIgQzcuODA2LDcwLjczIDguMjAyLDcwLjA0NyA4LjIwMiw2OS40ODcgQzguMjAyLDY5LjA5IDguMDAzLDY4Ljg2MSA3LjcxNCw2OC44NjEiIGlkPSJGaWxsLTE1IiBmaWxsPSIjODA5N0EyIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTcuNDQ0LDg1LjM1IEM3LjcwOCw4NS4xOTggNy45MjEsODUuMzE5IDcuOTIxLDg1LjYyMiBDNy45MjEsODUuOTI1IDcuNzA4LDg2LjI5MiA3LjQ0NCw4Ni40NDQgQzcuMTgxLDg2LjU5NyA2Ljk2Nyw4Ni40NzUgNi45NjcsODYuMTczIEM2Ljk2Nyw4NS44NzEgNy4xODEsODUuNTAyIDcuNDQ0LDg1LjM1IiBpZD0iRmlsbC0xNiIgZmlsbD0iI0ZGRkZGRiI+PC9wYXRoPgogICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik03LjIzLDg2LjUxIEM3LjA3NCw4Ni41MSA2Ljk2Nyw4Ni4zODcgNi45NjcsODYuMTczIEM2Ljk2Nyw4NS44NzEgNy4xODEsODUuNTAyIDcuNDQ0LDg1LjM1IEM3LjUyMSw4NS4zMDUgNy41OTQsODUuMjg0IDcuNjU4LDg1LjI4NCBDNy44MTQsODUuMjg0IDcuOTIxLDg1LjQwOCA3LjkyMSw4NS42MjIgQzcuOTIxLDg1LjkyNSA3LjcwOCw4Ni4yOTIgNy40NDQsODYuNDQ0IEM3LjM2Nyw4Ni40ODkgNy4yOTQsODYuNTEgNy4yMyw4Ni41MSBNNy42NTgsODUuMDk4IEM3LjU1OCw4NS4wOTggNy40NTUsODUuMTI3IDcuMzUxLDg1LjE4OCBDNy4wMzEsODUuMzczIDYuNzgxLDg1LjgwNiA2Ljc4MSw4Ni4xNzMgQzYuNzgxLDg2LjQ4MiA2Ljk2Niw4Ni42OTcgNy4yMyw4Ni42OTcgQzcuMzMsODYuNjk3IDcuNDMzLDg2LjY2NiA3LjUzOCw4Ni42MDcgQzcuODU4LDg2LjQyMiA4LjEwOCw4NS45ODkgOC4xMDgsODUuNjIyIEM4LjEwOCw4NS4zMTMgNy45MjMsODUuMDk4IDcuNjU4LDg1LjA5OCIgaWQ9IkZpbGwtMTciIGZpbGw9IiM4MDk3QTIiPjwvcGF0aD4KICAgICAgICAgICAgICAgICAgICA8cGF0aCBkPSJNNy4yMyw4Ni4zMjIgTDcuMTU0LDg2LjE3MyBDNy4xNTQsODUuOTM4IDcuMzMzLDg1LjYyOSA3LjUzOCw4NS41MTIgTDcuNjU4LDg1LjQ3MSBMNy43MzQsODUuNjIyIEM3LjczNCw4NS44NTYgNy41NTUsODYuMTY0IDcuMzUxLDg2LjI4MiBMNy4yMyw4Ni4zMjIgTTcuNjU4LDg1LjI4NCBDNy41OTQsODUuMjg0IDcuNTIxLDg1LjMwNSA3LjQ0NCw4NS4zNSBDNy4xODEsODUuNTAyIDYuOTY3LDg1Ljg3MSA2Ljk2Nyw4Ni4xNzMgQzYuOTY3LDg2LjM4NyA3LjA3NCw4Ni41MSA3LjIzLDg2LjUxIEM3LjI5NCw4Ni41MSA3LjM2Nyw4Ni40ODkgNy40NDQsODYuNDQ0IEM3LjcwOCw4Ni4yOTIgNy45MjEsODUuOTI1IDcuOTIxLDg1LjYyMiBDNy45MjEsODUuNDA4IDcuODE0LDg1LjI4NCA3LjY1OCw4NS4yODQiIGlkPSJGaWxsLTE4IiBmaWxsPSIjODA5N0EyIj48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTc3LjI3OCw3Ljc2OSBMNzcuMjc4LDUxLjQzNiBMMTAuMjA4LDkwLjE2IEwxMC4yMDgsNDYuNDkzIEw3Ny4yNzgsNy43NjkiIGlkPSJGaWxsLTE5IiBmaWxsPSIjNDU1QTY0Ij48L3BhdGg+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTEwLjA4Myw5MC4zNzUgTDEwLjA4Myw0Ni40MjEgTDEwLjE0Niw0Ni4zODUgTDc3LjQwMyw3LjU1NCBMNzcuNDAzLDUxLjUwOCBMNzcuMzQxLDUxLjU0NCBMMTAuMDgzLDkwLjM3NSBMMTAuMDgzLDkwLjM3NSBaIE0xMC4zMzMsNDYuNTY0IEwxMC4zMzMsODkuOTQ0IEw3Ny4xNTQsNTEuMzY1IEw3Ny4xNTQsNy45ODUgTDEwLjMzMyw0Ni41NjQgTDEwLjMzMyw0Ni41NjQgWiIgaWQ9IkZpbGwtMjAiIGZpbGw9IiM2MDdEOEIiPjwvcGF0aD4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0xMjUuNzM3LDg4LjY0NyBMMTE4LjA5OCw5MS45ODEgTDExOC4wOTgsODQgTDEwNi42MzksODguNzEzIEwxMDYuNjM5LDk2Ljk4MiBMOTksMTAwLjMxNSBMMTEyLjM2OSwxMDMuOTYxIEwxMjUuNzM3LDg4LjY0NyIgaWQ9IkltcG9ydGVkLUxheWVycy1Db3B5LTIiIGZpbGw9IiM0NTVBNjQiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+');
|
|
};
|
|
|
|
module.exports = RotateInstructions;
|
|
|
|
},{"./util.js":22}],17:[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.
|
|
*/
|
|
|
|
/**
|
|
* TODO: Fix up all "new THREE" instantiations to improve performance.
|
|
*/
|
|
var SensorSample = _dereq_('./sensor-sample.js');
|
|
var MathUtil = _dereq_('../math-util.js');
|
|
var Util = _dereq_('../util.js');
|
|
|
|
var DEBUG = false;
|
|
|
|
/**
|
|
* An implementation of a simple complementary filter, which fuses gyroscope and
|
|
* accelerometer data from the 'devicemotion' event.
|
|
*
|
|
* Accelerometer data is very noisy, but stable over the long term.
|
|
* Gyroscope data is smooth, but tends to drift over the long term.
|
|
*
|
|
* This fusion is relatively simple:
|
|
* 1. Get orientation estimates from accelerometer by applying a low-pass filter
|
|
* on that data.
|
|
* 2. Get orientation estimates from gyroscope by integrating over time.
|
|
* 3. Combine the two estimates, weighing (1) in the long term, but (2) for the
|
|
* short term.
|
|
*/
|
|
function ComplementaryFilter(kFilter) {
|
|
this.kFilter = kFilter;
|
|
|
|
// Raw sensor measurements.
|
|
this.currentAccelMeasurement = new SensorSample();
|
|
this.currentGyroMeasurement = new SensorSample();
|
|
this.previousGyroMeasurement = new SensorSample();
|
|
|
|
// Set the quaternion to be looking in the -z direction by default.
|
|
this.filterQ = new MathUtil.Quaternion(1, 0, 0, 1);
|
|
this.previousFilterQ = new MathUtil.Quaternion();
|
|
|
|
// Orientation based on the accelerometer.
|
|
this.accelQ = new MathUtil.Quaternion();
|
|
// Whether or not the orientation has been initialized.
|
|
this.isOrientationInitialized = false;
|
|
// Running estimate of gravity based on the current orientation.
|
|
this.estimatedGravity = new MathUtil.Vector3();
|
|
// Measured gravity based on accelerometer.
|
|
this.measuredGravity = new MathUtil.Vector3();
|
|
|
|
// Debug only quaternion of gyro-based orientation.
|
|
this.gyroIntegralQ = new MathUtil.Quaternion();
|
|
}
|
|
|
|
ComplementaryFilter.prototype.addAccelMeasurement = function(vector, timestampS) {
|
|
this.currentAccelMeasurement.set(vector, timestampS);
|
|
};
|
|
|
|
ComplementaryFilter.prototype.addGyroMeasurement = function(vector, timestampS) {
|
|
this.currentGyroMeasurement.set(vector, timestampS);
|
|
|
|
var deltaT = timestampS - this.previousGyroMeasurement.timestampS;
|
|
if (Util.isTimestampDeltaValid(deltaT)) {
|
|
this.run_();
|
|
}
|
|
|
|
this.previousGyroMeasurement.copy(this.currentGyroMeasurement);
|
|
};
|
|
|
|
ComplementaryFilter.prototype.run_ = function() {
|
|
|
|
if (!this.isOrientationInitialized) {
|
|
this.accelQ = this.accelToQuaternion_(this.currentAccelMeasurement.sample);
|
|
this.previousFilterQ.copy(this.accelQ);
|
|
this.isOrientationInitialized = true;
|
|
return;
|
|
}
|
|
|
|
var deltaT = this.currentGyroMeasurement.timestampS -
|
|
this.previousGyroMeasurement.timestampS;
|
|
|
|
// Convert gyro rotation vector to a quaternion delta.
|
|
var gyroDeltaQ = this.gyroToQuaternionDelta_(this.currentGyroMeasurement.sample, deltaT);
|
|
this.gyroIntegralQ.multiply(gyroDeltaQ);
|
|
|
|
// filter_1 = K * (filter_0 + gyro * dT) + (1 - K) * accel.
|
|
this.filterQ.copy(this.previousFilterQ);
|
|
this.filterQ.multiply(gyroDeltaQ);
|
|
|
|
// Calculate the delta between the current estimated gravity and the real
|
|
// gravity vector from accelerometer.
|
|
var invFilterQ = new MathUtil.Quaternion();
|
|
invFilterQ.copy(this.filterQ);
|
|
invFilterQ.inverse();
|
|
|
|
this.estimatedGravity.set(0, 0, -1);
|
|
this.estimatedGravity.applyQuaternion(invFilterQ);
|
|
this.estimatedGravity.normalize();
|
|
|
|
this.measuredGravity.copy(this.currentAccelMeasurement.sample);
|
|
this.measuredGravity.normalize();
|
|
|
|
// Compare estimated gravity with measured gravity, get the delta quaternion
|
|
// between the two.
|
|
var deltaQ = new MathUtil.Quaternion();
|
|
deltaQ.setFromUnitVectors(this.estimatedGravity, this.measuredGravity);
|
|
deltaQ.inverse();
|
|
|
|
if (DEBUG) {
|
|
console.log('Delta: %d deg, G_est: (%s, %s, %s), G_meas: (%s, %s, %s)',
|
|
MathUtil.radToDeg * Util.getQuaternionAngle(deltaQ),
|
|
(this.estimatedGravity.x).toFixed(1),
|
|
(this.estimatedGravity.y).toFixed(1),
|
|
(this.estimatedGravity.z).toFixed(1),
|
|
(this.measuredGravity.x).toFixed(1),
|
|
(this.measuredGravity.y).toFixed(1),
|
|
(this.measuredGravity.z).toFixed(1));
|
|
}
|
|
|
|
// Calculate the SLERP target: current orientation plus the measured-estimated
|
|
// quaternion delta.
|
|
var targetQ = new MathUtil.Quaternion();
|
|
targetQ.copy(this.filterQ);
|
|
targetQ.multiply(deltaQ);
|
|
|
|
// SLERP factor: 0 is pure gyro, 1 is pure accel.
|
|
this.filterQ.slerp(targetQ, 1 - this.kFilter);
|
|
|
|
this.previousFilterQ.copy(this.filterQ);
|
|
};
|
|
|
|
ComplementaryFilter.prototype.getOrientation = function() {
|
|
return this.filterQ;
|
|
};
|
|
|
|
ComplementaryFilter.prototype.accelToQuaternion_ = function(accel) {
|
|
var normAccel = new MathUtil.Vector3();
|
|
normAccel.copy(accel);
|
|
normAccel.normalize();
|
|
var quat = new MathUtil.Quaternion();
|
|
quat.setFromUnitVectors(new MathUtil.Vector3(0, 0, -1), normAccel);
|
|
quat.inverse();
|
|
return quat;
|
|
};
|
|
|
|
ComplementaryFilter.prototype.gyroToQuaternionDelta_ = function(gyro, dt) {
|
|
// Extract axis and angle from the gyroscope data.
|
|
var quat = new MathUtil.Quaternion();
|
|
var axis = new MathUtil.Vector3();
|
|
axis.copy(gyro);
|
|
axis.normalize();
|
|
quat.setFromAxisAngle(axis, gyro.length() * dt);
|
|
return quat;
|
|
};
|
|
|
|
|
|
module.exports = ComplementaryFilter;
|
|
|
|
},{"../math-util.js":14,"../util.js":22,"./sensor-sample.js":20}],18:[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 ComplementaryFilter = _dereq_('./complementary-filter.js');
|
|
var PosePredictor = _dereq_('./pose-predictor.js');
|
|
var TouchPanner = _dereq_('../touch-panner.js');
|
|
var MathUtil = _dereq_('../math-util.js');
|
|
var Util = _dereq_('../util.js');
|
|
|
|
/**
|
|
* The pose sensor, implemented using DeviceMotion APIs.
|
|
*/
|
|
function FusionPoseSensor() {
|
|
this.deviceId = 'webvr-polyfill:fused';
|
|
this.deviceName = 'VR Position Device (webvr-polyfill:fused)';
|
|
|
|
this.accelerometer = new MathUtil.Vector3();
|
|
this.gyroscope = new MathUtil.Vector3();
|
|
|
|
window.addEventListener('devicemotion', this.onDeviceMotionChange_.bind(this));
|
|
window.addEventListener('orientationchange', this.onScreenOrientationChange_.bind(this));
|
|
|
|
this.filter = new ComplementaryFilter(WebVRConfig.K_FILTER);
|
|
this.posePredictor = new PosePredictor(WebVRConfig.PREDICTION_TIME_S);
|
|
this.touchPanner = new TouchPanner();
|
|
|
|
this.filterToWorldQ = new MathUtil.Quaternion();
|
|
|
|
// Set the filter to world transform, depending on OS.
|
|
if (Util.isIOS()) {
|
|
this.filterToWorldQ.setFromAxisAngle(new MathUtil.Vector3(1, 0, 0), Math.PI / 2);
|
|
} else {
|
|
this.filterToWorldQ.setFromAxisAngle(new MathUtil.Vector3(1, 0, 0), -Math.PI / 2);
|
|
}
|
|
|
|
this.inverseWorldToScreenQ = new MathUtil.Quaternion();
|
|
this.worldToScreenQ = new MathUtil.Quaternion();
|
|
this.originalPoseAdjustQ = new MathUtil.Quaternion();
|
|
this.originalPoseAdjustQ.setFromAxisAngle(new MathUtil.Vector3(0, 0, 1),
|
|
-window.orientation * Math.PI / 180);
|
|
|
|
this.setScreenTransform_();
|
|
// Adjust this filter for being in landscape mode.
|
|
if (Util.isLandscapeMode()) {
|
|
this.filterToWorldQ.multiply(this.inverseWorldToScreenQ);
|
|
}
|
|
|
|
// Keep track of a reset transform for resetSensor.
|
|
this.resetQ = new MathUtil.Quaternion();
|
|
|
|
this.isFirefoxAndroid = Util.isFirefoxAndroid();
|
|
this.isIOS = Util.isIOS();
|
|
|
|
this.orientationOut_ = new Float32Array(4);
|
|
}
|
|
|
|
FusionPoseSensor.prototype.getPosition = function() {
|
|
// This PoseSensor doesn't support position
|
|
return null;
|
|
};
|
|
|
|
FusionPoseSensor.prototype.getOrientation = function() {
|
|
// Convert from filter space to the the same system used by the
|
|
// deviceorientation event.
|
|
var orientation = this.filter.getOrientation();
|
|
|
|
// Predict orientation.
|
|
this.predictedQ = this.posePredictor.getPrediction(orientation, this.gyroscope, this.previousTimestampS);
|
|
|
|
// Convert to THREE coordinate system: -Z forward, Y up, X right.
|
|
var out = new MathUtil.Quaternion();
|
|
out.copy(this.filterToWorldQ);
|
|
out.multiply(this.resetQ);
|
|
if (!WebVRConfig.TOUCH_PANNER_DISABLED) {
|
|
out.multiply(this.touchPanner.getOrientation());
|
|
}
|
|
out.multiply(this.predictedQ);
|
|
out.multiply(this.worldToScreenQ);
|
|
|
|
// Handle the yaw-only case.
|
|
if (WebVRConfig.YAW_ONLY) {
|
|
// Make a quaternion that only turns around the Y-axis.
|
|
out.x = 0;
|
|
out.z = 0;
|
|
out.normalize();
|
|
}
|
|
|
|
this.orientationOut_[0] = out.x;
|
|
this.orientationOut_[1] = out.y;
|
|
this.orientationOut_[2] = out.z;
|
|
this.orientationOut_[3] = out.w;
|
|
return this.orientationOut_;
|
|
};
|
|
|
|
FusionPoseSensor.prototype.resetPose = function() {
|
|
// Reduce to inverted yaw-only.
|
|
this.resetQ.copy(this.filter.getOrientation());
|
|
this.resetQ.x = 0;
|
|
this.resetQ.y = 0;
|
|
this.resetQ.z *= -1;
|
|
this.resetQ.normalize();
|
|
|
|
// Take into account extra transformations in landscape mode.
|
|
if (Util.isLandscapeMode()) {
|
|
this.resetQ.multiply(this.inverseWorldToScreenQ);
|
|
}
|
|
|
|
// Take into account original pose.
|
|
this.resetQ.multiply(this.originalPoseAdjustQ);
|
|
|
|
if (!WebVRConfig.TOUCH_PANNER_DISABLED) {
|
|
this.touchPanner.resetSensor();
|
|
}
|
|
};
|
|
|
|
FusionPoseSensor.prototype.onDeviceMotionChange_ = function(deviceMotion) {
|
|
var accGravity = deviceMotion.accelerationIncludingGravity;
|
|
var rotRate = deviceMotion.rotationRate;
|
|
var timestampS = deviceMotion.timeStamp / 1000;
|
|
|
|
// Firefox Android timeStamp returns one thousandth of a millisecond.
|
|
if (this.isFirefoxAndroid) {
|
|
timestampS /= 1000;
|
|
}
|
|
|
|
var deltaS = timestampS - this.previousTimestampS;
|
|
if (deltaS <= Util.MIN_TIMESTEP || deltaS > Util.MAX_TIMESTEP) {
|
|
console.warn('Invalid timestamps detected. Time step between successive ' +
|
|
'gyroscope sensor samples is very small or not monotonic');
|
|
this.previousTimestampS = timestampS;
|
|
return;
|
|
}
|
|
this.accelerometer.set(-accGravity.x, -accGravity.y, -accGravity.z);
|
|
this.gyroscope.set(rotRate.alpha, rotRate.beta, rotRate.gamma);
|
|
|
|
// With iOS and Firefox Android, rotationRate is reported in degrees,
|
|
// so we first convert to radians.
|
|
if (this.isIOS || this.isFirefoxAndroid) {
|
|
this.gyroscope.multiplyScalar(Math.PI / 180);
|
|
}
|
|
|
|
this.filter.addAccelMeasurement(this.accelerometer, timestampS);
|
|
this.filter.addGyroMeasurement(this.gyroscope, timestampS);
|
|
|
|
this.previousTimestampS = timestampS;
|
|
};
|
|
|
|
FusionPoseSensor.prototype.onScreenOrientationChange_ =
|
|
function(screenOrientation) {
|
|
this.setScreenTransform_();
|
|
};
|
|
|
|
FusionPoseSensor.prototype.setScreenTransform_ = function() {
|
|
this.worldToScreenQ.set(0, 0, 0, 1);
|
|
switch (window.orientation) {
|
|
case 0:
|
|
break;
|
|
case 90:
|
|
this.worldToScreenQ.setFromAxisAngle(new MathUtil.Vector3(0, 0, 1), -Math.PI / 2);
|
|
break;
|
|
case -90:
|
|
this.worldToScreenQ.setFromAxisAngle(new MathUtil.Vector3(0, 0, 1), Math.PI / 2);
|
|
break;
|
|
case 180:
|
|
// TODO.
|
|
break;
|
|
}
|
|
this.inverseWorldToScreenQ.copy(this.worldToScreenQ);
|
|
this.inverseWorldToScreenQ.inverse();
|
|
};
|
|
|
|
module.exports = FusionPoseSensor;
|
|
|
|
},{"../math-util.js":14,"../touch-panner.js":21,"../util.js":22,"./complementary-filter.js":17,"./pose-predictor.js":19}],19:[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 MathUtil = _dereq_('../math-util.js');
|
|
var DEBUG = false;
|
|
|
|
/**
|
|
* Given an orientation and the gyroscope data, predicts the future orientation
|
|
* of the head. This makes rendering appear faster.
|
|
*
|
|
* Also see: http://msl.cs.uiuc.edu/~lavalle/papers/LavYerKatAnt14.pdf
|
|
*
|
|
* @param {Number} predictionTimeS time from head movement to the appearance of
|
|
* the corresponding image.
|
|
*/
|
|
function PosePredictor(predictionTimeS) {
|
|
this.predictionTimeS = predictionTimeS;
|
|
|
|
// The quaternion corresponding to the previous state.
|
|
this.previousQ = new MathUtil.Quaternion();
|
|
// Previous time a prediction occurred.
|
|
this.previousTimestampS = null;
|
|
|
|
// The delta quaternion that adjusts the current pose.
|
|
this.deltaQ = new MathUtil.Quaternion();
|
|
// The output quaternion.
|
|
this.outQ = new MathUtil.Quaternion();
|
|
}
|
|
|
|
PosePredictor.prototype.getPrediction = function(currentQ, gyro, timestampS) {
|
|
if (!this.previousTimestampS) {
|
|
this.previousQ.copy(currentQ);
|
|
this.previousTimestampS = timestampS;
|
|
return currentQ;
|
|
}
|
|
|
|
// Calculate axis and angle based on gyroscope rotation rate data.
|
|
var axis = new MathUtil.Vector3();
|
|
axis.copy(gyro);
|
|
axis.normalize();
|
|
|
|
var angularSpeed = gyro.length();
|
|
|
|
// If we're rotating slowly, don't do prediction.
|
|
if (angularSpeed < MathUtil.degToRad * 20) {
|
|
if (DEBUG) {
|
|
console.log('Moving slowly, at %s deg/s: no prediction',
|
|
(MathUtil.radToDeg * angularSpeed).toFixed(1));
|
|
}
|
|
this.outQ.copy(currentQ);
|
|
this.previousQ.copy(currentQ);
|
|
return this.outQ;
|
|
}
|
|
|
|
// Get the predicted angle based on the time delta and latency.
|
|
var deltaT = timestampS - this.previousTimestampS;
|
|
var predictAngle = angularSpeed * this.predictionTimeS;
|
|
|
|
this.deltaQ.setFromAxisAngle(axis, predictAngle);
|
|
this.outQ.copy(this.previousQ);
|
|
this.outQ.multiply(this.deltaQ);
|
|
|
|
this.previousQ.copy(currentQ);
|
|
|
|
return this.outQ;
|
|
};
|
|
|
|
|
|
module.exports = PosePredictor;
|
|
|
|
},{"../math-util.js":14}],20:[function(_dereq_,module,exports){
|
|
function SensorSample(sample, timestampS) {
|
|
this.set(sample, timestampS);
|
|
};
|
|
|
|
SensorSample.prototype.set = function(sample, timestampS) {
|
|
this.sample = sample;
|
|
this.timestampS = timestampS;
|
|
};
|
|
|
|
SensorSample.prototype.copy = function(sensorSample) {
|
|
this.set(sensorSample.sample, sensorSample.timestampS);
|
|
};
|
|
|
|
module.exports = SensorSample;
|
|
|
|
},{}],21:[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 MathUtil = _dereq_('./math-util.js');
|
|
var Util = _dereq_('./util.js');
|
|
|
|
var ROTATE_SPEED = 0.5;
|
|
/**
|
|
* Provides a quaternion responsible for pre-panning the scene before further
|
|
* transformations due to device sensors.
|
|
*/
|
|
function TouchPanner() {
|
|
window.addEventListener('touchstart', this.onTouchStart_.bind(this));
|
|
window.addEventListener('touchmove', this.onTouchMove_.bind(this));
|
|
window.addEventListener('touchend', this.onTouchEnd_.bind(this));
|
|
|
|
this.isTouching = false;
|
|
this.rotateStart = new MathUtil.Vector2();
|
|
this.rotateEnd = new MathUtil.Vector2();
|
|
this.rotateDelta = new MathUtil.Vector2();
|
|
|
|
this.theta = 0;
|
|
this.orientation = new MathUtil.Quaternion();
|
|
}
|
|
|
|
TouchPanner.prototype.getOrientation = function() {
|
|
this.orientation.setFromEulerXYZ(0, 0, this.theta);
|
|
return this.orientation;
|
|
};
|
|
|
|
TouchPanner.prototype.resetSensor = function() {
|
|
this.theta = 0;
|
|
};
|
|
|
|
TouchPanner.prototype.onTouchStart_ = function(e) {
|
|
// Only respond if there is exactly one touch.
|
|
if (e.touches.length != 1) {
|
|
return;
|
|
}
|
|
this.rotateStart.set(e.touches[0].pageX, e.touches[0].pageY);
|
|
this.isTouching = true;
|
|
};
|
|
|
|
TouchPanner.prototype.onTouchMove_ = function(e) {
|
|
if (!this.isTouching) {
|
|
return;
|
|
}
|
|
this.rotateEnd.set(e.touches[0].pageX, e.touches[0].pageY);
|
|
this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart);
|
|
this.rotateStart.copy(this.rotateEnd);
|
|
|
|
// On iOS, direction is inverted.
|
|
if (Util.isIOS()) {
|
|
this.rotateDelta.x *= -1;
|
|
}
|
|
|
|
var element = document.body;
|
|
this.theta += 2 * Math.PI * this.rotateDelta.x / element.clientWidth * ROTATE_SPEED;
|
|
};
|
|
|
|
TouchPanner.prototype.onTouchEnd_ = function(e) {
|
|
this.isTouching = false;
|
|
};
|
|
|
|
module.exports = TouchPanner;
|
|
|
|
},{"./math-util.js":14,"./util.js":22}],22:[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 objectAssign = _dereq_('object-assign');
|
|
|
|
var Util = window.Util || {};
|
|
|
|
Util.MIN_TIMESTEP = 0.001;
|
|
Util.MAX_TIMESTEP = 1;
|
|
|
|
Util.base64 = function(mimeType, base64) {
|
|
return 'data:' + mimeType + ';base64,' + base64;
|
|
};
|
|
|
|
Util.clamp = function(value, min, max) {
|
|
return Math.min(Math.max(min, value), max);
|
|
};
|
|
|
|
Util.lerp = function(a, b, t) {
|
|
return a + ((b - a) * t);
|
|
};
|
|
|
|
Util.isIOS = (function() {
|
|
var isIOS = /iPad|iPhone|iPod/.test(navigator.platform);
|
|
return function() {
|
|
return isIOS;
|
|
};
|
|
})();
|
|
|
|
Util.isSafari = (function() {
|
|
var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
return function() {
|
|
return isSafari;
|
|
};
|
|
})();
|
|
|
|
Util.isFirefoxAndroid = (function() {
|
|
var isFirefoxAndroid = navigator.userAgent.indexOf('Firefox') !== -1 &&
|
|
navigator.userAgent.indexOf('Android') !== -1;
|
|
return function() {
|
|
return isFirefoxAndroid;
|
|
};
|
|
})();
|
|
|
|
Util.isLandscapeMode = function() {
|
|
return (window.orientation == 90 || window.orientation == -90);
|
|
};
|
|
|
|
// Helper method to validate the time steps of sensor timestamps.
|
|
Util.isTimestampDeltaValid = function(timestampDeltaS) {
|
|
if (isNaN(timestampDeltaS)) {
|
|
return false;
|
|
}
|
|
if (timestampDeltaS <= Util.MIN_TIMESTEP) {
|
|
return false;
|
|
}
|
|
if (timestampDeltaS > Util.MAX_TIMESTEP) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
Util.getScreenWidth = function() {
|
|
return Math.max(window.screen.width, window.screen.height) *
|
|
window.devicePixelRatio;
|
|
};
|
|
|
|
Util.getScreenHeight = function() {
|
|
return Math.min(window.screen.width, window.screen.height) *
|
|
window.devicePixelRatio;
|
|
};
|
|
|
|
Util.requestFullscreen = function(element) {
|
|
if (element.requestFullscreen) {
|
|
element.requestFullscreen();
|
|
} else if (element.webkitRequestFullscreen) {
|
|
element.webkitRequestFullscreen();
|
|
} else if (element.mozRequestFullScreen) {
|
|
element.mozRequestFullScreen();
|
|
} else if (element.msRequestFullscreen) {
|
|
element.msRequestFullscreen();
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
Util.exitFullscreen = function() {
|
|
if (document.exitFullscreen) {
|
|
document.exitFullscreen();
|
|
} else if (document.webkitExitFullscreen) {
|
|
document.webkitExitFullscreen();
|
|
} else if (document.mozCancelFullScreen) {
|
|
document.mozCancelFullScreen();
|
|
} else if (document.msExitFullscreen) {
|
|
document.msExitFullscreen();
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
Util.getFullscreenElement = function() {
|
|
return document.fullscreenElement ||
|
|
document.webkitFullscreenElement ||
|
|
document.mozFullScreenElement ||
|
|
document.msFullscreenElement;
|
|
};
|
|
|
|
Util.linkProgram = function(gl, vertexSource, fragmentSource, attribLocationMap) {
|
|
// No error checking for brevity.
|
|
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
gl.shaderSource(vertexShader, vertexSource);
|
|
gl.compileShader(vertexShader);
|
|
|
|
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
gl.shaderSource(fragmentShader, fragmentSource);
|
|
gl.compileShader(fragmentShader);
|
|
|
|
var program = gl.createProgram();
|
|
gl.attachShader(program, vertexShader);
|
|
gl.attachShader(program, fragmentShader);
|
|
|
|
for (var attribName in attribLocationMap)
|
|
gl.bindAttribLocation(program, attribLocationMap[attribName], attribName);
|
|
|
|
gl.linkProgram(program);
|
|
|
|
gl.deleteShader(vertexShader);
|
|
gl.deleteShader(fragmentShader);
|
|
|
|
return program;
|
|
};
|
|
|
|
Util.getProgramUniforms = function(gl, program) {
|
|
var uniforms = {};
|
|
var uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS);
|
|
var uniformName = '';
|
|
for (var i = 0; i < uniformCount; i++) {
|
|
var uniformInfo = gl.getActiveUniform(program, i);
|
|
uniformName = uniformInfo.name.replace('[0]', '');
|
|
uniforms[uniformName] = gl.getUniformLocation(program, uniformName);
|
|
}
|
|
return uniforms;
|
|
};
|
|
|
|
Util.orthoMatrix = function (out, left, right, bottom, top, near, far) {
|
|
var lr = 1 / (left - right),
|
|
bt = 1 / (bottom - top),
|
|
nf = 1 / (near - far);
|
|
out[0] = -2 * lr;
|
|
out[1] = 0;
|
|
out[2] = 0;
|
|
out[3] = 0;
|
|
out[4] = 0;
|
|
out[5] = -2 * bt;
|
|
out[6] = 0;
|
|
out[7] = 0;
|
|
out[8] = 0;
|
|
out[9] = 0;
|
|
out[10] = 2 * nf;
|
|
out[11] = 0;
|
|
out[12] = (left + right) * lr;
|
|
out[13] = (top + bottom) * bt;
|
|
out[14] = (far + near) * nf;
|
|
out[15] = 1;
|
|
return out;
|
|
};
|
|
|
|
Util.isMobile = function() {
|
|
var check = false;
|
|
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
|
|
return check;
|
|
};
|
|
|
|
Util.extend = objectAssign;
|
|
|
|
Util.safariCssSizeWorkaround = function(canvas) {
|
|
// TODO(smus): Remove this workaround when Safari for iOS is fixed.
|
|
// iOS only workaround (for https://bugs.webkit.org/show_bug.cgi?id=152556).
|
|
//
|
|
// "To the last I grapple with thee;
|
|
// from hell's heart I stab at thee;
|
|
// for hate's sake I spit my last breath at thee."
|
|
// -- Moby Dick, by Herman Melville
|
|
if (Util.isIOS()) {
|
|
var width = canvas.style.width;
|
|
var height = canvas.style.height;
|
|
canvas.style.width = (parseInt(width) + 1) + 'px';
|
|
canvas.style.height = (parseInt(height)) + 'px';
|
|
console.log('Resetting width to...', width);
|
|
setTimeout(function() {
|
|
console.log('Done. Width is now', width);
|
|
canvas.style.width = width;
|
|
canvas.style.height = height;
|
|
}, 100);
|
|
}
|
|
|
|
// Debug only.
|
|
window.Util = Util;
|
|
window.canvas = canvas;
|
|
};
|
|
|
|
Util.frameDataFromPose = (function() {
|
|
var piOver180 = Math.PI / 180.0;
|
|
var rad45 = Math.PI * 0.25;
|
|
|
|
// Borrowed from glMatrix.
|
|
function mat4_perspectiveFromFieldOfView(out, fov, near, far) {
|
|
var upTan = Math.tan(fov ? (fov.upDegrees * piOver180) : rad45),
|
|
downTan = Math.tan(fov ? (fov.downDegrees * piOver180) : rad45),
|
|
leftTan = Math.tan(fov ? (fov.leftDegrees * piOver180) : rad45),
|
|
rightTan = Math.tan(fov ? (fov.rightDegrees * piOver180) : rad45),
|
|
xScale = 2.0 / (leftTan + rightTan),
|
|
yScale = 2.0 / (upTan + downTan);
|
|
|
|
out[0] = xScale;
|
|
out[1] = 0.0;
|
|
out[2] = 0.0;
|
|
out[3] = 0.0;
|
|
out[4] = 0.0;
|
|
out[5] = yScale;
|
|
out[6] = 0.0;
|
|
out[7] = 0.0;
|
|
out[8] = -((leftTan - rightTan) * xScale * 0.5);
|
|
out[9] = ((upTan - downTan) * yScale * 0.5);
|
|
out[10] = far / (near - far);
|
|
out[11] = -1.0;
|
|
out[12] = 0.0;
|
|
out[13] = 0.0;
|
|
out[14] = (far * near) / (near - far);
|
|
out[15] = 0.0;
|
|
return out;
|
|
}
|
|
|
|
function mat4_fromRotationTranslation(out, q, v) {
|
|
// Quaternion math
|
|
var x = q[0], y = q[1], z = q[2], w = q[3],
|
|
x2 = x + x,
|
|
y2 = y + y,
|
|
z2 = z + z,
|
|
|
|
xx = x * x2,
|
|
xy = x * y2,
|
|
xz = x * z2,
|
|
yy = y * y2,
|
|
yz = y * z2,
|
|
zz = z * z2,
|
|
wx = w * x2,
|
|
wy = w * y2,
|
|
wz = w * z2;
|
|
|
|
out[0] = 1 - (yy + zz);
|
|
out[1] = xy + wz;
|
|
out[2] = xz - wy;
|
|
out[3] = 0;
|
|
out[4] = xy - wz;
|
|
out[5] = 1 - (xx + zz);
|
|
out[6] = yz + wx;
|
|
out[7] = 0;
|
|
out[8] = xz + wy;
|
|
out[9] = yz - wx;
|
|
out[10] = 1 - (xx + yy);
|
|
out[11] = 0;
|
|
out[12] = v[0];
|
|
out[13] = v[1];
|
|
out[14] = v[2];
|
|
out[15] = 1;
|
|
|
|
return out;
|
|
};
|
|
|
|
function mat4_translate(out, a, v) {
|
|
var x = v[0], y = v[1], z = v[2],
|
|
a00, a01, a02, a03,
|
|
a10, a11, a12, a13,
|
|
a20, a21, a22, a23;
|
|
|
|
if (a === out) {
|
|
out[12] = a[0] * x + a[4] * y + a[8] * z + a[12];
|
|
out[13] = a[1] * x + a[5] * y + a[9] * z + a[13];
|
|
out[14] = a[2] * x + a[6] * y + a[10] * z + a[14];
|
|
out[15] = a[3] * x + a[7] * y + a[11] * z + a[15];
|
|
} else {
|
|
a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3];
|
|
a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7];
|
|
a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11];
|
|
|
|
out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03;
|
|
out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13;
|
|
out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23;
|
|
|
|
out[12] = a00 * x + a10 * y + a20 * z + a[12];
|
|
out[13] = a01 * x + a11 * y + a21 * z + a[13];
|
|
out[14] = a02 * x + a12 * y + a22 * z + a[14];
|
|
out[15] = a03 * x + a13 * y + a23 * z + a[15];
|
|
}
|
|
|
|
return out;
|
|
};
|
|
|
|
mat4_invert = function(out, a) {
|
|
var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3],
|
|
a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7],
|
|
a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11],
|
|
a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15],
|
|
|
|
b00 = a00 * a11 - a01 * a10,
|
|
b01 = a00 * a12 - a02 * a10,
|
|
b02 = a00 * a13 - a03 * a10,
|
|
b03 = a01 * a12 - a02 * a11,
|
|
b04 = a01 * a13 - a03 * a11,
|
|
b05 = a02 * a13 - a03 * a12,
|
|
b06 = a20 * a31 - a21 * a30,
|
|
b07 = a20 * a32 - a22 * a30,
|
|
b08 = a20 * a33 - a23 * a30,
|
|
b09 = a21 * a32 - a22 * a31,
|
|
b10 = a21 * a33 - a23 * a31,
|
|
b11 = a22 * a33 - a23 * a32,
|
|
|
|
// Calculate the determinant
|
|
det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
|
|
|
|
if (!det) {
|
|
return null;
|
|
}
|
|
det = 1.0 / det;
|
|
|
|
out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
|
|
out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
|
|
out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
|
|
out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
|
|
out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
|
|
out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
|
|
out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
|
|
out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
|
|
out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
|
|
out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
|
|
out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
|
|
out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
|
|
out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
|
|
out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
|
|
out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
|
|
out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
|
|
|
|
return out;
|
|
};
|
|
|
|
var defaultOrientation = new Float32Array([0, 0, 0, 1]);
|
|
var defaultPosition = new Float32Array([0, 0, 0]);
|
|
|
|
function updateEyeMatrices(projection, view, pose, parameters, vrDisplay) {
|
|
mat4_perspectiveFromFieldOfView(projection, parameters ? parameters.fieldOfView : null, vrDisplay.depthNear, vrDisplay.depthFar);
|
|
|
|
var orientation = pose.orientation || defaultOrientation;
|
|
var position = pose.position || defaultPosition;
|
|
|
|
mat4_fromRotationTranslation(view, orientation, position);
|
|
if (parameters)
|
|
mat4_translate(view, view, parameters.offset);
|
|
mat4_invert(view, view);
|
|
}
|
|
|
|
return function(frameData, pose, vrDisplay) {
|
|
if (!frameData || !pose)
|
|
return false;
|
|
|
|
frameData.pose = pose;
|
|
frameData.timestamp = pose.timestamp;
|
|
|
|
updateEyeMatrices(
|
|
frameData.leftProjectionMatrix, frameData.leftViewMatrix,
|
|
pose, vrDisplay.getEyeParameters("left"), vrDisplay);
|
|
updateEyeMatrices(
|
|
frameData.rightProjectionMatrix, frameData.rightViewMatrix,
|
|
pose, vrDisplay.getEyeParameters("right"), vrDisplay);
|
|
|
|
return true;
|
|
};
|
|
})();
|
|
|
|
module.exports = Util;
|
|
|
|
},{"object-assign":1}],23:[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 Emitter = _dereq_('./emitter.js');
|
|
var Util = _dereq_('./util.js');
|
|
var DeviceInfo = _dereq_('./device-info.js');
|
|
|
|
var DEFAULT_VIEWER = 'CardboardV1';
|
|
var VIEWER_KEY = 'WEBVR_CARDBOARD_VIEWER';
|
|
var CLASS_NAME = 'webvr-polyfill-viewer-selector';
|
|
|
|
/**
|
|
* Creates a viewer selector with the options specified. Supports being shown
|
|
* and hidden. Generates events when viewer parameters change. Also supports
|
|
* saving the currently selected index in localStorage.
|
|
*/
|
|
function ViewerSelector() {
|
|
// Try to load the selected key from local storage. If none exists, use the
|
|
// default key.
|
|
try {
|
|
this.selectedKey = localStorage.getItem(VIEWER_KEY) || DEFAULT_VIEWER;
|
|
} catch (error) {
|
|
console.error('Failed to load viewer profile: %s', error);
|
|
}
|
|
this.dialog = this.createDialog_(DeviceInfo.Viewers);
|
|
this.root = null;
|
|
}
|
|
ViewerSelector.prototype = new Emitter();
|
|
|
|
ViewerSelector.prototype.show = function(root) {
|
|
this.root = root;
|
|
|
|
root.appendChild(this.dialog);
|
|
//console.log('ViewerSelector.show');
|
|
|
|
// Ensure the currently selected item is checked.
|
|
var selected = this.dialog.querySelector('#' + this.selectedKey);
|
|
selected.checked = true;
|
|
|
|
// Show the UI.
|
|
this.dialog.style.display = 'block';
|
|
};
|
|
|
|
ViewerSelector.prototype.hide = function() {
|
|
if (this.root && this.root.contains(this.dialog)) {
|
|
this.root.removeChild(this.dialog);
|
|
}
|
|
//console.log('ViewerSelector.hide');
|
|
this.dialog.style.display = 'none';
|
|
};
|
|
|
|
ViewerSelector.prototype.getCurrentViewer = function() {
|
|
return DeviceInfo.Viewers[this.selectedKey];
|
|
};
|
|
|
|
ViewerSelector.prototype.getSelectedKey_ = function() {
|
|
var input = this.dialog.querySelector('input[name=field]:checked');
|
|
if (input) {
|
|
return input.id;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
ViewerSelector.prototype.onSave_ = function() {
|
|
this.selectedKey = this.getSelectedKey_();
|
|
if (!this.selectedKey || !DeviceInfo.Viewers[this.selectedKey]) {
|
|
console.error('ViewerSelector.onSave_: this should never happen!');
|
|
return;
|
|
}
|
|
|
|
this.emit('change', DeviceInfo.Viewers[this.selectedKey]);
|
|
|
|
// Attempt to save the viewer profile, but fails in private mode.
|
|
try {
|
|
localStorage.setItem(VIEWER_KEY, this.selectedKey);
|
|
} catch(error) {
|
|
console.error('Failed to save viewer profile: %s', error);
|
|
}
|
|
this.hide();
|
|
};
|
|
|
|
/**
|
|
* Creates the dialog.
|
|
*/
|
|
ViewerSelector.prototype.createDialog_ = function(options) {
|
|
var container = document.createElement('div');
|
|
container.classList.add(CLASS_NAME);
|
|
container.style.display = 'none';
|
|
// Create an overlay that dims the background, and which goes away when you
|
|
// tap it.
|
|
var overlay = document.createElement('div');
|
|
var s = overlay.style;
|
|
s.position = 'fixed';
|
|
s.left = 0;
|
|
s.top = 0;
|
|
s.width = '100%';
|
|
s.height = '100%';
|
|
s.background = 'rgba(0, 0, 0, 0.3)';
|
|
overlay.addEventListener('click', this.hide.bind(this));
|
|
|
|
var width = 280;
|
|
var dialog = document.createElement('div');
|
|
var s = dialog.style;
|
|
s.boxSizing = 'border-box';
|
|
s.position = 'fixed';
|
|
s.top = '24px';
|
|
s.left = '50%';
|
|
s.marginLeft = (-width/2) + 'px';
|
|
s.width = width + 'px';
|
|
s.padding = '24px';
|
|
s.overflow = 'hidden';
|
|
s.background = '#fafafa';
|
|
s.fontFamily = "'Roboto', sans-serif";
|
|
s.boxShadow = '0px 5px 20px #666';
|
|
|
|
dialog.appendChild(this.createH1_('Select your viewer'));
|
|
for (var id in options) {
|
|
dialog.appendChild(this.createChoice_(id, options[id].label));
|
|
}
|
|
dialog.appendChild(this.createButton_('Save', this.onSave_.bind(this)));
|
|
|
|
container.appendChild(overlay);
|
|
container.appendChild(dialog);
|
|
|
|
return container;
|
|
};
|
|
|
|
ViewerSelector.prototype.createH1_ = function(name) {
|
|
var h1 = document.createElement('h1');
|
|
var s = h1.style;
|
|
s.color = 'black';
|
|
s.fontSize = '20px';
|
|
s.fontWeight = 'bold';
|
|
s.marginTop = 0;
|
|
s.marginBottom = '24px';
|
|
h1.innerHTML = name;
|
|
return h1;
|
|
};
|
|
|
|
ViewerSelector.prototype.createChoice_ = function(id, name) {
|
|
/*
|
|
<div class="choice">
|
|
<input id="v1" type="radio" name="field" value="v1">
|
|
<label for="v1">Cardboard V1</label>
|
|
</div>
|
|
*/
|
|
var div = document.createElement('div');
|
|
div.style.marginTop = '8px';
|
|
div.style.color = 'black';
|
|
|
|
var input = document.createElement('input');
|
|
input.style.fontSize = '30px';
|
|
input.setAttribute('id', id);
|
|
input.setAttribute('type', 'radio');
|
|
input.setAttribute('value', id);
|
|
input.setAttribute('name', 'field');
|
|
|
|
var label = document.createElement('label');
|
|
label.style.marginLeft = '4px';
|
|
label.setAttribute('for', id);
|
|
label.innerHTML = name;
|
|
|
|
div.appendChild(input);
|
|
div.appendChild(label);
|
|
|
|
return div;
|
|
};
|
|
|
|
ViewerSelector.prototype.createButton_ = function(label, onclick) {
|
|
var button = document.createElement('button');
|
|
button.innerHTML = label;
|
|
var s = button.style;
|
|
s.float = 'right';
|
|
s.textTransform = 'uppercase';
|
|
s.color = '#1094f7';
|
|
s.fontSize = '14px';
|
|
s.letterSpacing = 0;
|
|
s.border = 0;
|
|
s.background = 'none';
|
|
s.marginTop = '16px';
|
|
|
|
button.addEventListener('click', onclick);
|
|
|
|
return button;
|
|
};
|
|
|
|
module.exports = ViewerSelector;
|
|
|
|
},{"./device-info.js":7,"./emitter.js":12,"./util.js":22}],24:[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');
|
|
|
|
/**
|
|
* Android and iOS compatible wakelock implementation.
|
|
*
|
|
* Refactored thanks to dkovalev@.
|
|
*/
|
|
function AndroidWakeLock() {
|
|
var video = document.createElement('video');
|
|
|
|
video.addEventListener('ended', function() {
|
|
video.play();
|
|
});
|
|
|
|
this.request = function() {
|
|
if (video.paused) {
|
|
// Base64 version of videos_src/no-sleep-120s.mp4.
|
|
video.src = Util.base64('video/mp4', 'AAAAGGZ0eXBpc29tAAAAAG1wNDFhdmMxAAAIA21vb3YAAABsbXZoZAAAAADSa9v60mvb+gABX5AAlw/gAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAdkdHJhawAAAFx0a2hkAAAAAdJr2/rSa9v6AAAAAQAAAAAAlw/gAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAQAAAAHAAAAAAAJGVkdHMAAAAcZWxzdAAAAAAAAAABAJcP4AAAAAAAAQAAAAAG3G1kaWEAAAAgbWRoZAAAAADSa9v60mvb+gAPQkAGjneAFccAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAABodtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAZHc3RibAAAAJdzdHNkAAAAAAAAAAEAAACHYXZjMQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAMABwASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAADFhdmNDAWQAC//hABlnZAALrNlfllw4QAAAAwBAAAADAKPFCmWAAQAFaOvssiwAAAAYc3R0cwAAAAAAAAABAAAAbgAPQkAAAAAUc3RzcwAAAAAAAAABAAAAAQAAA4BjdHRzAAAAAAAAAG4AAAABAD0JAAAAAAEAehIAAAAAAQA9CQAAAAABAAAAAAAAAAEAD0JAAAAAAQBMS0AAAAABAB6EgAAAAAEAAAAAAAAAAQAPQkAAAAABAExLQAAAAAEAHoSAAAAAAQAAAAAAAAABAA9CQAAAAAEATEtAAAAAAQAehIAAAAABAAAAAAAAAAEAD0JAAAAAAQBMS0AAAAABAB6EgAAAAAEAAAAAAAAAAQAPQkAAAAABAExLQAAAAAEAHoSAAAAAAQAAAAAAAAABAA9CQAAAAAEATEtAAAAAAQAehIAAAAABAAAAAAAAAAEAD0JAAAAAAQBMS0AAAAABAB6EgAAAAAEAAAAAAAAAAQAPQkAAAAABAExLQAAAAAEAHoSAAAAAAQAAAAAAAAABAA9CQAAAAAEATEtAAAAAAQAehIAAAAABAAAAAAAAAAEAD0JAAAAAAQBMS0AAAAABAB6EgAAAAAEAAAAAAAAAAQAPQkAAAAABAExLQAAAAAEAHoSAAAAAAQAAAAAAAAABAA9CQAAAAAEATEtAAAAAAQAehIAAAAABAAAAAAAAAAEAD0JAAAAAAQBMS0AAAAABAB6EgAAAAAEAAAAAAAAAAQAPQkAAAAABAExLQAAAAAEAHoSAAAAAAQAAAAAAAAABAA9CQAAAAAEATEtAAAAAAQAehIAAAAABAAAAAAAAAAEAD0JAAAAAAQBMS0AAAAABAB6EgAAAAAEAAAAAAAAAAQAPQkAAAAABAExLQAAAAAEAHoSAAAAAAQAAAAAAAAABAA9CQAAAAAEATEtAAAAAAQAehIAAAAABAAAAAAAAAAEAD0JAAAAAAQBMS0AAAAABAB6EgAAAAAEAAAAAAAAAAQAPQkAAAAABAExLQAAAAAEAHoSAAAAAAQAAAAAAAAABAA9CQAAAAAEATEtAAAAAAQAehIAAAAABAAAAAAAAAAEAD0JAAAAAAQBMS0AAAAABAB6EgAAAAAEAAAAAAAAAAQAPQkAAAAABAExLQAAAAAEAHoSAAAAAAQAAAAAAAAABAA9CQAAAAAEATEtAAAAAAQAehIAAAAABAAAAAAAAAAEAD0JAAAAAAQBMS0AAAAABAB6EgAAAAAEAAAAAAAAAAQAPQkAAAAABAExLQAAAAAEAHoSAAAAAAQAAAAAAAAABAA9CQAAAAAEALcbAAAAAHHN0c2MAAAAAAAAAAQAAAAEAAABuAAAAAQAAAcxzdHN6AAAAAAAAAAAAAABuAAADCQAAABgAAAAOAAAADgAAAAwAAAASAAAADgAAAAwAAAAMAAAAEgAAAA4AAAAMAAAADAAAABIAAAAOAAAADAAAAAwAAAASAAAADgAAAAwAAAAMAAAAEgAAAA4AAAAMAAAADAAAABIAAAAOAAAADAAAAAwAAAASAAAADgAAAAwAAAAMAAAAEgAAAA4AAAAMAAAADAAAABIAAAAOAAAADAAAAAwAAAASAAAADgAAAAwAAAAMAAAAEgAAAA4AAAAMAAAADAAAABIAAAAOAAAADAAAAAwAAAASAAAADgAAAAwAAAAMAAAAEgAAAA4AAAAMAAAADAAAABIAAAAOAAAADAAAAAwAAAASAAAADgAAAAwAAAAMAAAAEgAAAA4AAAAMAAAADAAAABIAAAAOAAAADAAAAAwAAAASAAAADgAAAAwAAAAMAAAAEgAAAA4AAAAMAAAADAAAABIAAAAOAAAADAAAAAwAAAASAAAADgAAAAwAAAAMAAAAEgAAAA4AAAAMAAAADAAAABIAAAAOAAAADAAAAAwAAAASAAAADgAAAAwAAAAMAAAAEgAAAA4AAAAMAAAADAAAABMAAAAUc3RjbwAAAAAAAAABAAAIKwAAACt1ZHRhAAAAI6llbmMAFwAAdmxjIDIuMi4xIHN0cmVhbSBvdXRwdXQAAAAId2lkZQAACRRtZGF0AAACrgX//6vcRem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTQyIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxNCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDEzIG1lPWhleCBzdWJtZT03IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0xIDh4OGRjdD0xIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJvbWFfcXBfb2Zmc2V0PS0yIHRocmVhZHM9MTIgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEgc2NlbmVjdXQ9NDAgaW50cmFfcmVmcmVzaD0wIHJjX2xvb2thaGVhZD00MCByYz1hYnIgbWJ0cmVlPTEgYml0cmF0ZT0xMDAgcmF0ZXRvbD0xLjAgcWNvbXA9MC42MCBxcG1pbj0xMCBxcG1heD01MSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAU2WIhAAQ/8ltlOe+cTZuGkKg+aRtuivcDZ0pBsfsEi9p/i1yU9DxS2lq4dXTinViF1URBKXgnzKBd/Uh1bkhHtMrwrRcOJslD01UB+fyaL6ef+DBAAAAFEGaJGxBD5B+v+a+4QqF3MgBXz9MAAAACkGeQniH/+94r6EAAAAKAZ5hdEN/8QytwAAAAAgBnmNqQ3/EgQAAAA5BmmhJqEFomUwIIf/+4QAAAApBnoZFESw//76BAAAACAGepXRDf8SBAAAACAGep2pDf8SAAAAADkGarEmoQWyZTAgh//7gAAAACkGeykUVLD//voEAAAAIAZ7pdEN/xIAAAAAIAZ7rakN/xIAAAAAOQZrwSahBbJlMCCH//uEAAAAKQZ8ORRUsP/++gQAAAAgBny10Q3/EgQAAAAgBny9qQ3/EgAAAAA5BmzRJqEFsmUwIIf/+4AAAAApBn1JFFSw//76BAAAACAGfcXRDf8SAAAAACAGfc2pDf8SAAAAADkGbeEmoQWyZTAgh//7hAAAACkGflkUVLD//voAAAAAIAZ+1dEN/xIEAAAAIAZ+3akN/xIEAAAAOQZu8SahBbJlMCCH//uAAAAAKQZ/aRRUsP/++gQAAAAgBn/l0Q3/EgAAAAAgBn/tqQ3/EgQAAAA5Bm+BJqEFsmUwIIf/+4QAAAApBnh5FFSw//76AAAAACAGePXRDf8SAAAAACAGeP2pDf8SBAAAADkGaJEmoQWyZTAgh//7gAAAACkGeQkUVLD//voEAAAAIAZ5hdEN/xIAAAAAIAZ5jakN/xIEAAAAOQZpoSahBbJlMCCH//uEAAAAKQZ6GRRUsP/++gQAAAAgBnqV0Q3/EgQAAAAgBnqdqQ3/EgAAAAA5BmqxJqEFsmUwIIf/+4AAAAApBnspFFSw//76BAAAACAGe6XRDf8SAAAAACAGe62pDf8SAAAAADkGa8EmoQWyZTAgh//7hAAAACkGfDkUVLD//voEAAAAIAZ8tdEN/xIEAAAAIAZ8vakN/xIAAAAAOQZs0SahBbJlMCCH//uAAAAAKQZ9SRRUsP/++gQAAAAgBn3F0Q3/EgAAAAAgBn3NqQ3/EgAAAAA5Bm3hJqEFsmUwIIf/+4QAAAApBn5ZFFSw//76AAAAACAGftXRDf8SBAAAACAGft2pDf8SBAAAADkGbvEmoQWyZTAgh//7gAAAACkGf2kUVLD//voEAAAAIAZ/5dEN/xIAAAAAIAZ/7akN/xIEAAAAOQZvgSahBbJlMCCH//uEAAAAKQZ4eRRUsP/++gAAAAAgBnj10Q3/EgAAAAAgBnj9qQ3/EgQAAAA5BmiRJqEFsmUwIIf/+4AAAAApBnkJFFSw//76BAAAACAGeYXRDf8SAAAAACAGeY2pDf8SBAAAADkGaaEmoQWyZTAgh//7hAAAACkGehkUVLD//voEAAAAIAZ6ldEN/xIEAAAAIAZ6nakN/xIAAAAAOQZqsSahBbJlMCCH//uAAAAAKQZ7KRRUsP/++gQAAAAgBnul0Q3/EgAAAAAgBnutqQ3/EgAAAAA5BmvBJqEFsmUwIIf/+4QAAAApBnw5FFSw//76BAAAACAGfLXRDf8SBAAAACAGfL2pDf8SAAAAADkGbNEmoQWyZTAgh//7gAAAACkGfUkUVLD//voEAAAAIAZ9xdEN/xIAAAAAIAZ9zakN/xIAAAAAOQZt4SahBbJlMCCH//uEAAAAKQZ+WRRUsP/++gAAAAAgBn7V0Q3/EgQAAAAgBn7dqQ3/EgQAAAA5Bm7xJqEFsmUwIIf/+4AAAAApBn9pFFSw//76BAAAACAGf+XRDf8SAAAAACAGf+2pDf8SBAAAADkGb4EmoQWyZTAgh//7hAAAACkGeHkUVLD//voAAAAAIAZ49dEN/xIAAAAAIAZ4/akN/xIEAAAAOQZokSahBbJlMCCH//uAAAAAKQZ5CRRUsP/++gQAAAAgBnmF0Q3/EgAAAAAgBnmNqQ3/EgQAAAA5BmmhJqEFsmUwIIf/+4QAAAApBnoZFFSw//76BAAAACAGepXRDf8SBAAAACAGep2pDf8SAAAAADkGarEmoQWyZTAgh//7gAAAACkGeykUVLD//voEAAAAIAZ7pdEN/xIAAAAAIAZ7rakN/xIAAAAAPQZruSahBbJlMFEw3//7B');
|
|
video.play();
|
|
}
|
|
};
|
|
|
|
this.release = function() {
|
|
video.pause();
|
|
video.src = '';
|
|
};
|
|
}
|
|
|
|
function iOSWakeLock() {
|
|
var timer = null;
|
|
|
|
this.request = function() {
|
|
if (!timer) {
|
|
timer = setInterval(function() {
|
|
window.location = window.location;
|
|
setTimeout(window.stop, 0);
|
|
}, 30000);
|
|
}
|
|
}
|
|
|
|
this.release = function() {
|
|
if (timer) {
|
|
clearInterval(timer);
|
|
timer = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function getWakeLock() {
|
|
var userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
|
if (userAgent.match(/iPhone/i) || userAgent.match(/iPod/i)) {
|
|
return iOSWakeLock;
|
|
} else {
|
|
return AndroidWakeLock;
|
|
}
|
|
}
|
|
|
|
module.exports = getWakeLock();
|
|
},{"./util.js":22}],25:[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 CardboardVRDisplay = _dereq_('./cardboard-vr-display.js');
|
|
var MouseKeyboardVRDisplay = _dereq_('./mouse-keyboard-vr-display.js');
|
|
// Uncomment to add positional tracking via webcam.
|
|
//var WebcamPositionSensorVRDevice = require('./webcam-position-sensor-vr-device.js');
|
|
var VRDisplay = _dereq_('./base.js').VRDisplay;
|
|
var VRFrameData = _dereq_('./base.js').VRFrameData;
|
|
var HMDVRDevice = _dereq_('./base.js').HMDVRDevice;
|
|
var PositionSensorVRDevice = _dereq_('./base.js').PositionSensorVRDevice;
|
|
var VRDisplayHMDDevice = _dereq_('./display-wrappers.js').VRDisplayHMDDevice;
|
|
var VRDisplayPositionSensorDevice = _dereq_('./display-wrappers.js').VRDisplayPositionSensorDevice;
|
|
|
|
function WebVRPolyfill() {
|
|
this.displays = [];
|
|
this.devices = []; // For deprecated objects
|
|
this.devicesPopulated = false;
|
|
this.nativeWebVRAvailable = this.isWebVRAvailable();
|
|
this.nativeLegacyWebVRAvailable = this.isDeprecatedWebVRAvailable();
|
|
|
|
if (!this.nativeLegacyWebVRAvailable) {
|
|
if (!this.nativeWebVRAvailable) {
|
|
this.enablePolyfill();
|
|
}
|
|
if (WebVRConfig.ENABLE_DEPRECATED_API) {
|
|
this.enableDeprecatedPolyfill();
|
|
}
|
|
}
|
|
|
|
// Put a shim in place to update the API to 1.1 if needed.
|
|
InstallWebVRSpecShim();
|
|
}
|
|
|
|
WebVRPolyfill.prototype.isWebVRAvailable = function() {
|
|
return ('getVRDisplays' in navigator);
|
|
};
|
|
|
|
WebVRPolyfill.prototype.isDeprecatedWebVRAvailable = function() {
|
|
return ('getVRDevices' in navigator) || ('mozGetVRDevices' in navigator);
|
|
};
|
|
|
|
WebVRPolyfill.prototype.populateDevices = function() {
|
|
if (this.devicesPopulated) {
|
|
return;
|
|
}
|
|
|
|
// Initialize our virtual VR devices.
|
|
var vrDisplay = null;
|
|
|
|
// Add a Cardboard VRDisplay on compatible mobile devices
|
|
if (this.isCardboardCompatible()) {
|
|
vrDisplay = new CardboardVRDisplay();
|
|
this.displays.push(vrDisplay);
|
|
|
|
// For backwards compatibility
|
|
if (WebVRConfig.ENABLE_DEPRECATED_API) {
|
|
this.devices.push(new VRDisplayHMDDevice(vrDisplay));
|
|
this.devices.push(new VRDisplayPositionSensorDevice(vrDisplay));
|
|
}
|
|
}
|
|
|
|
// Add a Mouse and Keyboard driven VRDisplay for desktops/laptops
|
|
if (!this.isMobile() && !WebVRConfig.MOUSE_KEYBOARD_CONTROLS_DISABLED) {
|
|
vrDisplay = new MouseKeyboardVRDisplay();
|
|
this.displays.push(vrDisplay);
|
|
|
|
// For backwards compatibility
|
|
if (WebVRConfig.ENABLE_DEPRECATED_API) {
|
|
this.devices.push(new VRDisplayHMDDevice(vrDisplay));
|
|
this.devices.push(new VRDisplayPositionSensorDevice(vrDisplay));
|
|
}
|
|
}
|
|
|
|
// Uncomment to add positional tracking via webcam.
|
|
//if (!this.isMobile() && WebVRConfig.ENABLE_DEPRECATED_API) {
|
|
// positionDevice = new WebcamPositionSensorVRDevice();
|
|
// this.devices.push(positionDevice);
|
|
//}
|
|
|
|
this.devicesPopulated = true;
|
|
};
|
|
|
|
WebVRPolyfill.prototype.enablePolyfill = function() {
|
|
// Provide navigator.getVRDisplays.
|
|
navigator.getVRDisplays = this.getVRDisplays.bind(this);
|
|
|
|
// Provide the VRDisplay object.
|
|
window.VRDisplay = VRDisplay;
|
|
// Provide the VRFrameData object.
|
|
window.VRFrameData = VRFrameData;
|
|
};
|
|
|
|
WebVRPolyfill.prototype.enableDeprecatedPolyfill = function() {
|
|
// Provide navigator.getVRDevices.
|
|
navigator.getVRDevices = this.getVRDevices.bind(this);
|
|
|
|
// Provide the CardboardHMDVRDevice and PositionSensorVRDevice objects.
|
|
window.HMDVRDevice = HMDVRDevice;
|
|
window.PositionSensorVRDevice = PositionSensorVRDevice;
|
|
};
|
|
|
|
WebVRPolyfill.prototype.getVRDisplays = function() {
|
|
this.populateDevices();
|
|
var displays = this.displays;
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
resolve(displays);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
WebVRPolyfill.prototype.getVRDevices = function() {
|
|
console.warn('getVRDevices is deprecated. Please update your code to use getVRDisplays instead.');
|
|
var self = this;
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
if (!self.devicesPopulated) {
|
|
if (self.nativeWebVRAvailable) {
|
|
return navigator.getVRDisplays(function(displays) {
|
|
for (var i = 0; i < displays.length; ++i) {
|
|
self.devices.push(new VRDisplayHMDDevice(displays[i]));
|
|
self.devices.push(new VRDisplayPositionSensorDevice(displays[i]));
|
|
}
|
|
self.devicesPopulated = true;
|
|
resolve(self.devices);
|
|
}, reject);
|
|
}
|
|
|
|
if (self.nativeLegacyWebVRAvailable) {
|
|
return (navigator.getVRDDevices || navigator.mozGetVRDevices)(function(devices) {
|
|
for (var i = 0; i < devices.length; ++i) {
|
|
if (devices[i] instanceof HMDVRDevice) {
|
|
self.devices.push(devices[i]);
|
|
}
|
|
if (devices[i] instanceof PositionSensorVRDevice) {
|
|
self.devices.push(devices[i]);
|
|
}
|
|
}
|
|
self.devicesPopulated = true;
|
|
resolve(self.devices);
|
|
}, reject);
|
|
}
|
|
}
|
|
|
|
self.populateDevices();
|
|
resolve(self.devices);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Determine if a device is mobile.
|
|
*/
|
|
WebVRPolyfill.prototype.isMobile = function() {
|
|
return /Android/i.test(navigator.userAgent) ||
|
|
/iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
};
|
|
|
|
WebVRPolyfill.prototype.isCardboardCompatible = function() {
|
|
// For now, support all iOS and Android devices.
|
|
// Also enable the WebVRConfig.FORCE_VR flag for debugging.
|
|
return this.isMobile() || WebVRConfig.FORCE_ENABLE_VR;
|
|
};
|
|
|
|
// Installs a shim that updates a WebVR 1.0 spec implementation to WebVR 1.1
|
|
function InstallWebVRSpecShim() {
|
|
if ('VRDisplay' in window && !('VRFrameData' in window)) {
|
|
// Provide the VRFrameData object.
|
|
window.VRFrameData = VRFrameData;
|
|
|
|
// A lot of Chrome builds don't have depthNear and depthFar, even
|
|
// though they're in the WebVR 1.0 spec. Patch them in if they're not present.
|
|
if(!('depthNear' in window.VRDisplay.prototype)) {
|
|
window.VRDisplay.prototype.depthNear = 0.01;
|
|
}
|
|
|
|
if(!('depthFar' in window.VRDisplay.prototype)) {
|
|
window.VRDisplay.prototype.depthFar = 10000.0;
|
|
}
|
|
|
|
window.VRDisplay.prototype.getFrameData = function(frameData) {
|
|
return Util.frameDataFromPose(frameData, this.getPose(), this);
|
|
}
|
|
}
|
|
};
|
|
|
|
module.exports.WebVRPolyfill = WebVRPolyfill;
|
|
module.exports.InstallWebVRSpecShim = InstallWebVRSpecShim;
|
|
|
|
},{"./base.js":2,"./cardboard-vr-display.js":5,"./display-wrappers.js":8,"./mouse-keyboard-vr-display.js":15,"./util.js":22}]},{},[13]);
|