diff --git a/Cargo.lock b/Cargo.lock
index c01b17e394d8..7337d596445e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,24 +1,12 @@
[root]
-name = "webdriver_server"
+name = "webvr_traits"
version = "0.0.1"
dependencies = [
- "cookie 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "euclid 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)",
- "image 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)",
"ipc-channel 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"msg 0.0.1",
- "net_traits 0.0.1",
- "plugins 0.0.1",
- "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)",
- "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
- "script_traits 0.0.1",
- "servo_config 0.0.1",
- "servo_url 0.0.1",
- "url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "uuid 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "webdriver 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rust-webvr 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 0.8.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 0.8.20 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -445,6 +433,7 @@ dependencies = [
"servo_url 0.0.1",
"style_traits 0.0.1",
"webrender_traits 0.11.0 (git+https://github.com/servo/webrender)",
+ "webvr_traits 0.0.1",
]
[[package]]
@@ -1500,6 +1489,8 @@ dependencies = [
"webdriver_server 0.0.1",
"webrender 0.11.0 (git+https://github.com/servo/webrender)",
"webrender_traits 0.11.0 (git+https://github.com/servo/webrender)",
+ "webvr 0.0.1",
+ "webvr_traits 0.0.1",
]
[[package]]
@@ -2218,6 +2209,18 @@ name = "regex-syntax"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
+[[package]]
+name = "rust-webvr"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libloading 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 0.8.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 0.8.20 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.3"
@@ -2308,6 +2311,8 @@ dependencies = [
"uuid 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"webrender_traits 0.11.0 (git+https://github.com/servo/webrender)",
"websocket 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "webvr 0.0.1",
+ "webvr_traits 0.0.1",
"xml5ever 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -2379,6 +2384,7 @@ dependencies = [
"style_traits 0.0.1",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "webvr_traits 0.0.1",
]
[[package]]
@@ -3178,6 +3184,29 @@ dependencies = [
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "webdriver_server"
+version = "0.0.1"
+dependencies = [
+ "cookie 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "euclid 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hyper 0.9.14 (registry+https://github.com/rust-lang/crates.io-index)",
+ "image 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "ipc-channel 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "msg 0.0.1",
+ "net_traits 0.0.1",
+ "plugins 0.0.1",
+ "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
+ "script_traits 0.0.1",
+ "servo_config 0.0.1",
+ "servo_url 0.0.1",
+ "url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "uuid 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "webdriver 0.17.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "webrender"
version = "0.11.0"
@@ -3238,6 +3267,19 @@ dependencies = [
"url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "webvr"
+version = "0.0.1"
+dependencies = [
+ "ipc-channel 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "msg 0.0.1",
+ "script_traits 0.0.1",
+ "servo_config 0.0.1",
+ "webrender_traits 0.11.0 (git+https://github.com/servo/webrender)",
+ "webvr_traits 0.0.1",
+]
+
[[package]]
name = "winapi"
version = "0.2.8"
@@ -3500,6 +3542,7 @@ dependencies = [
"checksum ref_slice 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "825740057197b7d43025e7faf6477eaabc03434e153233da02d1f44602f71527"
"checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f"
"checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957"
+"checksum rust-webvr 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0f1c2770eade344950b6959fb7f4c658200a252a61f265b3487383b82fafe61e"
"checksum rustc-demangle 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1430d286cadb237c17c885e25447c982c97113926bb579f4379c0eca8d9586dc"
"checksum rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "237546c689f20bb44980270c73c3b9edd0891c1be49cc1274406134a66d3957b"
"checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084"
diff --git a/components/canvas/canvas_paint_thread.rs b/components/canvas/canvas_paint_thread.rs
index f7c3fdfb6ae9..447d60a28626 100644
--- a/components/canvas/canvas_paint_thread.rs
+++ b/components/canvas/canvas_paint_thread.rs
@@ -211,7 +211,8 @@ impl<'a> CanvasPaintThread<'a> {
}
}
}
- CanvasMsg::WebGL(_) => panic!("Wrong message sent to Canvas2D thread"),
+ CanvasMsg::WebGL(_) => panic!("Wrong WebGL message sent to Canvas2D thread"),
+ CanvasMsg::WebVR(_) => panic!("Wrong WebVR message sent to Canvas2D thread"),
}
}
}).expect("Thread spawning failed");
diff --git a/components/canvas/webgl_paint_thread.rs b/components/canvas/webgl_paint_thread.rs
index 7de4fd679792..e852742bc15b 100644
--- a/components/canvas/webgl_paint_thread.rs
+++ b/components/canvas/webgl_paint_thread.rs
@@ -144,6 +144,18 @@ impl WebGLPaintThread {
}
}
+ fn handle_webvr_message(&self, message: webrender_traits::VRCompositorCommand) {
+ match self.data {
+ WebGLPaintTaskData::WebRender(ref api, id) => {
+ api.send_vr_compositor_command(id, message);
+ }
+ WebGLPaintTaskData::Readback(..) => {
+ error!("Webrender is required for WebVR implementation");
+ }
+ }
+ }
+
+
/// Creates a new `WebGLPaintThread` and returns an `IpcSender` to
/// communicate with it.
pub fn start(size: Size2D,
@@ -190,6 +202,7 @@ impl WebGLPaintThread {
}
}
CanvasMsg::Canvas2d(_) => panic!("Wrong message sent to WebGLThread"),
+ CanvasMsg::WebVR(message) => painter.handle_webvr_message(message)
}
}
}).expect("Thread spawning failed");
diff --git a/components/canvas_traits/lib.rs b/components/canvas_traits/lib.rs
index e9e769bb51cd..50d431b9a6d5 100644
--- a/components/canvas_traits/lib.rs
+++ b/components/canvas_traits/lib.rs
@@ -27,7 +27,7 @@ use euclid::size::Size2D;
use ipc_channel::ipc::IpcSender;
use std::default::Default;
use std::str::FromStr;
-use webrender_traits::{WebGLCommand, WebGLContextId};
+use webrender_traits::{WebGLCommand, WebGLContextId, VRCompositorCommand};
#[derive(Clone, Deserialize, Serialize)]
pub enum FillRule {
@@ -42,6 +42,7 @@ pub enum CanvasMsg {
FromLayout(FromLayoutMsg),
FromScript(FromScriptMsg),
WebGL(WebGLCommand),
+ WebVR(VRCompositorCommand)
}
#[derive(Clone, Deserialize, Serialize)]
diff --git a/components/config/prefs.rs b/components/config/prefs.rs
index c0ad11bfe0bd..fd07383f2b21 100644
--- a/components/config/prefs.rs
+++ b/components/config/prefs.rs
@@ -259,4 +259,8 @@ impl Preferences {
pub fn extend(&self, extension: HashMap) {
self.0.write().unwrap().extend(extension);
}
+
+ pub fn is_webvr_enabled(&self) -> bool {
+ self.get("dom.webvr.enabled").as_boolean().unwrap_or(false)
+ }
}
diff --git a/components/constellation/Cargo.toml b/components/constellation/Cargo.toml
index 5299aa9bef13..b854d90e7647 100644
--- a/components/constellation/Cargo.toml
+++ b/components/constellation/Cargo.toml
@@ -36,6 +36,7 @@ servo_config = {path = "../config", features = ["servo"]}
servo_rand = {path = "../rand"}
servo_remutex = {path = "../remutex"}
servo_url = {path = "../url", features = ["servo"]}
+webvr_traits = {path = "../webvr_traits"}
[dependencies.webrender_traits]
git = "https://github.com/servo/webrender"
diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs
index 8adf2e6ab29f..fc67095dce77 100644
--- a/components/constellation/constellation.rs
+++ b/components/constellation/constellation.rs
@@ -101,6 +101,7 @@ use script_traits::{LayoutMsg as FromLayoutMsg, ScriptMsg as FromScriptMsg, Scri
use script_traits::{LogEntry, ServiceWorkerMsg, webdriver_msg};
use script_traits::{MozBrowserErrorType, MozBrowserEvent, WebDriverCommandMsg, WindowSizeData};
use script_traits::{SWManagerMsg, ScopeThings, WindowSizeType};
+use script_traits::WebVREventMsg;
use servo_config::opts;
use servo_config::prefs::PREFS;
use servo_rand::{Rng, SeedableRng, ServoRng, random};
@@ -122,6 +123,7 @@ use style_traits::cursor::Cursor;
use style_traits::viewport::ViewportConstraints;
use timer_scheduler::TimerScheduler;
use webrender_traits;
+use webvr_traits::WebVRMsg;
/// The `Constellation` itself. In the servo browser, there is one
/// constellation, which maintains all of the browser global data.
@@ -280,6 +282,9 @@ pub struct Constellation {
/// Phantom data that keeps the Rust type system happy.
phantom: PhantomData<(Message, LTF, STF)>,
+
+ /// A channel through which messages can be sent to the webvr thread.
+ webvr_thread: Option>,
}
/// State needed to construct a constellation.
@@ -535,6 +540,7 @@ impl Constellation
info!("Using seed {} for random pipeline closure.", seed);
(rng, prob)
}),
+ webvr_thread: None
};
constellation.run();
@@ -645,6 +651,7 @@ impl Constellation
prev_visibility: prev_visibility,
webrender_api_sender: self.webrender_api_sender.clone(),
is_private: is_private,
+ webvr_thread: self.webvr_thread.clone()
});
let pipeline = match result {
@@ -879,6 +886,14 @@ impl Constellation
FromCompositorMsg::LogEntry(top_level_frame_id, thread_name, entry) => {
self.handle_log_entry(top_level_frame_id, thread_name, entry);
}
+ FromCompositorMsg::SetWebVRThread(webvr_thread) => {
+ assert!(self.webvr_thread.is_none());
+ self.webvr_thread = Some(webvr_thread)
+ }
+ FromCompositorMsg::WebVREvent(pipeline_ids, event) => {
+ debug!("constellation got WebVR event");
+ self.handle_webvr_event(pipeline_ids, event);
+ }
}
}
@@ -1186,6 +1201,13 @@ impl Constellation
}
}
+ if let Some(chan) = self.webvr_thread.as_ref() {
+ debug!("Exiting WebVR thread.");
+ if let Err(e) = chan.send(WebVRMsg::Exit) {
+ warn!("Exit WebVR thread failed ({})", e);
+ }
+ }
+
debug!("Exiting font cache thread.");
self.font_cache_thread.exit();
@@ -1274,6 +1296,18 @@ impl Constellation
}
}
+ fn handle_webvr_event(&mut self, ids: Vec, event: WebVREventMsg) {
+ for id in ids {
+ match self.pipelines.get_mut(&id) {
+ Some(ref pipeline) => {
+ // Notify script thread
+ let _ = pipeline.event_loop.send(ConstellationControlMsg::WebVREvent(id, event.clone()));
+ },
+ None => warn!("constellation got webvr event for dead pipeline")
+ }
+ }
+ }
+
fn handle_init_load(&mut self, url: ServoUrl) {
let window_size = self.window_size.visible_viewport;
let root_pipeline_id = PipelineId::new();
diff --git a/components/constellation/lib.rs b/components/constellation/lib.rs
index a42e9fc7407b..6c983dbdf621 100644
--- a/components/constellation/lib.rs
+++ b/components/constellation/lib.rs
@@ -41,6 +41,7 @@ extern crate servo_remutex;
extern crate servo_url;
extern crate style_traits;
extern crate webrender_traits;
+extern crate webvr_traits;
mod constellation;
mod event_loop;
diff --git a/components/constellation/pipeline.rs b/components/constellation/pipeline.rs
index aa7d34616158..fba15eefcf73 100644
--- a/components/constellation/pipeline.rs
+++ b/components/constellation/pipeline.rs
@@ -36,6 +36,7 @@ use std::rc::Rc;
use std::sync::mpsc::Sender;
use style_traits::{PagePx, ViewportPx};
use webrender_traits;
+use webvr_traits::WebVRMsg;
/// A `Pipeline` is the constellation's view of a `Document`. Each pipeline has an
/// event loop (executed by a script thread) and a layout thread. A script thread
@@ -169,6 +170,8 @@ pub struct InitialPipelineState {
/// Whether this pipeline is considered private.
pub is_private: bool,
+ /// A channel to the webvr thread.
+ pub webvr_thread: Option>,
}
impl Pipeline {
@@ -268,6 +271,7 @@ impl Pipeline {
script_content_process_shutdown_chan: script_content_process_shutdown_chan,
script_content_process_shutdown_port: script_content_process_shutdown_port,
webrender_api_sender: state.webrender_api_sender,
+ webvr_thread: state.webvr_thread,
};
// Spawn the child process.
@@ -470,6 +474,7 @@ pub struct UnprivilegedPipelineContent {
script_content_process_shutdown_chan: IpcSender<()>,
script_content_process_shutdown_port: IpcReceiver<()>,
webrender_api_sender: webrender_traits::RenderApiSender,
+ webvr_thread: Option>,
}
impl UnprivilegedPipelineContent {
@@ -496,6 +501,7 @@ impl UnprivilegedPipelineContent {
window_size: self.window_size,
pipeline_namespace_id: self.pipeline_namespace_id,
content_process_shutdown_chan: self.script_content_process_shutdown_chan,
+ webvr_thread: self.webvr_thread
}, self.load_data.clone());
LTF::create(self.id,
diff --git a/components/profile/time.rs b/components/profile/time.rs
index e02de0393606..be3be16655fe 100644
--- a/components/profile/time.rs
+++ b/components/profile/time.rs
@@ -151,6 +151,7 @@ impl Formattable for ProfilerCategory {
ProfilerCategory::ScriptServiceWorkerEvent => "Script Service Worker Event",
ProfilerCategory::ScriptEnterFullscreen => "Script Enter Fullscreen",
ProfilerCategory::ScriptExitFullscreen => "Script Exit Fullscreen",
+ ProfilerCategory::ScriptWebVREvent => "Script WebVR Event",
ProfilerCategory::ApplicationHeartbeat => "Application Heartbeat",
};
format!("{}{}", padding, name)
diff --git a/components/profile_traits/time.rs b/components/profile_traits/time.rs
index f7b62e171396..7bbebf5f465d 100644
--- a/components/profile_traits/time.rs
+++ b/components/profile_traits/time.rs
@@ -88,6 +88,7 @@ pub enum ProfilerCategory {
ScriptParseXML = 0x76,
ScriptEnterFullscreen = 0x77,
ScriptExitFullscreen = 0x78,
+ ScriptWebVREvent = 0x79,
ApplicationHeartbeat = 0x90,
}
diff --git a/components/script/Cargo.toml b/components/script/Cargo.toml
index 10ea03bc5486..f980c2f92269 100644
--- a/components/script/Cargo.toml
+++ b/components/script/Cargo.toml
@@ -82,6 +82,8 @@ url = {version = "1.2", features = ["heap_size", "query_encoding"]}
uuid = {version = "0.3.1", features = ["v4"]}
websocket = "0.17"
xml5ever = {version = "0.3.1", features = ["unstable"]}
+webvr = {path = "../webvr"}
+webvr_traits = {path = "../webvr_traits"}
[dependencies.webrender_traits]
git = "https://github.com/servo/webrender"
diff --git a/components/script/dom/bindings/conversions.rs b/components/script/dom/bindings/conversions.rs
index 18a2deb8f0f5..577cecb2fd43 100644
--- a/components/script/dom/bindings/conversions.rs
+++ b/components/script/dom/bindings/conversions.rs
@@ -51,6 +51,9 @@ use js::jsapi::{JSObject, JSString, JS_GetArrayBufferViewType};
use js::jsapi::{JS_GetLatin1StringCharsAndLength, JS_GetObjectAsArrayBuffer, JS_GetObjectAsArrayBufferView};
use js::jsapi::{JS_GetReservedSlot, JS_GetTwoByteStringCharsAndLength};
use js::jsapi::{JS_IsArrayObject, JS_NewStringCopyN, JS_StringHasLatin1Chars};
+use js::jsapi::{JS_NewFloat32Array, JS_NewFloat64Array};
+use js::jsapi::{JS_NewInt8Array, JS_NewInt16Array, JS_NewInt32Array};
+use js::jsapi::{JS_NewUint8Array, JS_NewUint16Array, JS_NewUint32Array};
use js::jsapi::{MutableHandleValue, Type};
use js::jsval::{ObjectValue, StringValue};
use js::rust::{ToString, get_object_class, is_dom_class, is_dom_object, maybe_wrap_value};
@@ -463,6 +466,9 @@ pub unsafe trait ArrayBufferViewContents: Clone {
/// Check if the JS ArrayBufferView type is compatible with the implementor of the
/// trait
fn is_type_compatible(ty: Type) -> bool;
+
+ /// Creates a typed array
+ unsafe fn new(cx: *mut JSContext, num: u32) -> *mut JSObject;
}
unsafe impl ArrayBufferViewContents for u8 {
@@ -473,47 +479,79 @@ unsafe impl ArrayBufferViewContents for u8 {
_ => false,
}
}
+
+ unsafe fn new(cx: *mut JSContext, num: u32) -> *mut JSObject {
+ JS_NewUint8Array(cx, num)
+ }
}
unsafe impl ArrayBufferViewContents for i8 {
fn is_type_compatible(ty: Type) -> bool {
ty as i32 == Type::Int8 as i32
}
+
+ unsafe fn new(cx: *mut JSContext, num: u32) -> *mut JSObject {
+ JS_NewInt8Array(cx, num)
+ }
}
unsafe impl ArrayBufferViewContents for u16 {
fn is_type_compatible(ty: Type) -> bool {
ty as i32 == Type::Uint16 as i32
}
+
+ unsafe fn new(cx: *mut JSContext, num: u32) -> *mut JSObject {
+ JS_NewUint16Array(cx, num)
+ }
}
unsafe impl ArrayBufferViewContents for i16 {
fn is_type_compatible(ty: Type) -> bool {
ty as i32 == Type::Int16 as i32
}
+
+ unsafe fn new(cx: *mut JSContext, num: u32) -> *mut JSObject {
+ JS_NewInt16Array(cx, num)
+ }
}
unsafe impl ArrayBufferViewContents for u32 {
fn is_type_compatible(ty: Type) -> bool {
ty as i32 == Type::Uint32 as i32
}
+
+ unsafe fn new(cx: *mut JSContext, num: u32) -> *mut JSObject {
+ JS_NewUint32Array(cx, num)
+ }
}
unsafe impl ArrayBufferViewContents for i32 {
fn is_type_compatible(ty: Type) -> bool {
ty as i32 == Type::Int32 as i32
}
+
+ unsafe fn new(cx: *mut JSContext, num: u32) -> *mut JSObject {
+ JS_NewInt32Array(cx, num)
+ }
}
unsafe impl ArrayBufferViewContents for f32 {
fn is_type_compatible(ty: Type) -> bool {
ty as i32 == Type::Float32 as i32
}
+
+ unsafe fn new(cx: *mut JSContext, num: u32) -> *mut JSObject {
+ JS_NewFloat32Array(cx, num)
+ }
}
unsafe impl ArrayBufferViewContents for f64 {
fn is_type_compatible(ty: Type) -> bool {
ty as i32 == Type::Float64 as i32
}
+
+ unsafe fn new(cx: *mut JSContext, num: u32) -> *mut JSObject {
+ JS_NewFloat64Array(cx, num)
+ }
}
/// Returns a mutable slice of the Array Buffer View data, viewed as T, without
@@ -595,3 +633,23 @@ pub unsafe fn is_array_like(cx: *mut JSContext, value: HandleValue) -> bool {
assert!(JS_IsArrayObject(cx, value, &mut result));
result
}
+
+/// Creates a typed JS array from a Rust slice
+pub unsafe fn slice_to_array_buffer_view(cx: *mut JSContext, data: &[T]) -> *mut JSObject
+ where T: ArrayBufferViewContents
+{
+ let js_object = T::new(cx, data.len() as u32);
+ assert!(!js_object.is_null());
+ update_array_buffer_view(js_object, data);
+ js_object
+}
+
+/// Updates a typed JS array from a Rust slice
+pub unsafe fn update_array_buffer_view(obj: *mut JSObject, data: &[T])
+ where T: ArrayBufferViewContents
+{
+ let mut buffer = array_buffer_view_data(obj);
+ if let Some(ref mut buffer) = buffer {
+ ptr::copy_nonoverlapping(&data[0], &mut buffer[0], data.len())
+ }
+}
diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs
index 4f94ac8309fc..55b56977cd9b 100644
--- a/components/script/dom/mod.rs
+++ b/components/script/dom/mod.rs
@@ -429,6 +429,15 @@ pub mod validation;
pub mod validitystate;
pub mod values;
pub mod virtualmethods;
+pub mod vr;
+pub mod vrdisplay;
+pub mod vrdisplaycapabilities;
+pub mod vrdisplayevent;
+pub mod vreyeparameters;
+pub mod vrfieldofview;
+pub mod vrframedata;
+pub mod vrpose;
+pub mod vrstageparameters;
pub mod webgl_validations;
pub mod webglactiveinfo;
pub mod webglbuffer;
diff --git a/components/script/dom/navigator.rs b/components/script/dom/navigator.rs
index b7d781f5eb09..dd9a1a789c65 100644
--- a/components/script/dom/navigator.rs
+++ b/components/script/dom/navigator.rs
@@ -12,7 +12,9 @@ use dom::mimetypearray::MimeTypeArray;
use dom::navigatorinfo;
use dom::pluginarray::PluginArray;
use dom::serviceworkercontainer::ServiceWorkerContainer;
+use dom::vr::VR;
use dom::window::Window;
+use script_traits::WebVREventMsg;
#[dom_struct]
pub struct Navigator {
@@ -21,6 +23,7 @@ pub struct Navigator {
plugins: MutNullableJS,
mime_types: MutNullableJS,
service_worker: MutNullableJS,
+ vr: MutNullableJS
}
impl Navigator {
@@ -31,6 +34,7 @@ impl Navigator {
plugins: Default::default(),
mime_types: Default::default(),
service_worker: Default::default(),
+ vr: Default::default(),
}
}
@@ -114,4 +118,16 @@ impl NavigatorMethods for Navigator {
true
}
+ #[allow(unrooted_must_root)]
+ // https://w3c.github.io/webvr/#interface-navigator
+ fn Vr(&self) -> Root {
+ self.vr.or_init(|| VR::new(&self.global()))
+ }
+}
+
+impl Navigator {
+ pub fn handle_webvr_event(&self, event: WebVREventMsg) {
+ self.vr.get().expect("Shouldn't arrive here with an empty VR instance")
+ .handle_webvr_event(event);
+ }
}
diff --git a/components/script/dom/vr.rs b/components/script/dom/vr.rs
new file mode 100644
index 000000000000..6e077c415acd
--- /dev/null
+++ b/components/script/dom/vr.rs
@@ -0,0 +1,160 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use dom::bindings::cell::DOMRefCell;
+use dom::bindings::codegen::Bindings::VRBinding;
+use dom::bindings::codegen::Bindings::VRBinding::VRMethods;
+use dom::bindings::error::Error;
+use dom::bindings::inheritance::Castable;
+use dom::bindings::js::{JS, Root};
+use dom::bindings::reflector::{DomObject, reflect_dom_object};
+use dom::event::Event;
+use dom::eventtarget::EventTarget;
+use dom::globalscope::GlobalScope;
+use dom::promise::Promise;
+use dom::vrdisplay::VRDisplay;
+use dom::vrdisplayevent::VRDisplayEvent;
+use ipc_channel::ipc;
+use ipc_channel::ipc::IpcSender;
+use script_traits::WebVREventMsg;
+use std::rc::Rc;
+use webvr_traits::WebVRMsg;
+use webvr_traits::webvr;
+
+#[dom_struct]
+pub struct VR {
+ eventtarget: EventTarget,
+ displays: DOMRefCell>>
+}
+
+impl VR {
+ fn new_inherited() -> VR {
+ VR {
+ eventtarget: EventTarget::new_inherited(),
+ displays: DOMRefCell::new(Vec::new())
+ }
+ }
+
+ pub fn new(global: &GlobalScope) -> Root {
+ let root = reflect_dom_object(box VR::new_inherited(),
+ global,
+ VRBinding::Wrap);
+ root.register();
+ root
+ }
+}
+
+impl Drop for VR {
+ fn drop(&mut self) {
+ self.unregister();
+ }
+}
+
+impl VRMethods for VR {
+ #[allow(unrooted_must_root)]
+ // https://w3c.github.io/webvr/#interface-navigator
+ fn GetDisplays(&self) -> Rc {
+ let promise = Promise::new(&self.global());
+
+ if let Some(webvr_thread) = self.webvr_thread() {
+ let (sender, receiver) = ipc::channel().unwrap();
+ webvr_thread.send(WebVRMsg::GetDisplays(sender)).unwrap();
+ match receiver.recv().unwrap() {
+ Ok(displays) => {
+ // Sync displays
+ for display in displays {
+ self.sync_display(&display);
+ }
+ },
+ Err(e) => {
+ promise.reject_native(promise.global().get_cx(), &e);
+ return promise;
+ }
+ }
+ } else {
+ // WebVR spec: The Promise MUST be rejected if WebVR is not enabled/supported.
+ promise.reject_error(promise.global().get_cx(), Error::Security);
+ return promise;
+ }
+
+ // convert from JS to Root
+ let displays: Vec> = self.displays.borrow().iter()
+ .map(|d| Root::from_ref(&**d))
+ .collect();
+ promise.resolve_native(promise.global().get_cx(), &displays);
+
+ promise
+ }
+}
+
+
+impl VR {
+ fn webvr_thread(&self) -> Option> {
+ self.global().as_window().webvr_thread()
+ }
+
+ fn find_display(&self, display_id: u64) -> Option> {
+ self.displays.borrow()
+ .iter()
+ .find(|d| d.get_display_id() == display_id)
+ .map(|d| Root::from_ref(&**d))
+ }
+
+ fn register(&self) {
+ if let Some(webvr_thread) = self.webvr_thread() {
+ let msg = WebVRMsg::RegisterContext(self.global().pipeline_id());
+ webvr_thread.send(msg).unwrap();
+ }
+ }
+
+ fn unregister(&self) {
+ if let Some(webvr_thread) = self.webvr_thread() {
+ let msg = WebVRMsg::UnregisterContext(self.global().pipeline_id());
+ webvr_thread.send(msg).unwrap();
+ }
+ }
+
+ fn sync_display(&self, display: &webvr::VRDisplayData) -> Root {
+ if let Some(existing) = self.find_display(display.display_id) {
+ existing.update_display(&display);
+ existing
+ } else {
+ let root = VRDisplay::new(&self.global(), display.clone());
+ self.displays.borrow_mut().push(JS::from_ref(&*root));
+ root
+ }
+ }
+
+ pub fn handle_webvr_event(&self, event: WebVREventMsg) {
+ let WebVREventMsg::DisplayEvent(event) = event;
+ match &event {
+ &webvr::VRDisplayEvent::Connect(ref display) => {
+ let display = self.sync_display(&display);
+ display.handle_webvr_event(&event);
+ self.notify_event(&display, &event);
+ },
+ &webvr::VRDisplayEvent::Disconnect(id) => {
+ if let Some(display) = self.find_display(id) {
+ display.handle_webvr_event(&event);
+ self.notify_event(&display, &event);
+ }
+ },
+ &webvr::VRDisplayEvent::Activate(ref display, _) |
+ &webvr::VRDisplayEvent::Deactivate(ref display, _) |
+ &webvr::VRDisplayEvent::Blur(ref display) |
+ &webvr::VRDisplayEvent::Focus(ref display) |
+ &webvr::VRDisplayEvent::PresentChange(ref display, _) |
+ &webvr::VRDisplayEvent::Change(ref display) => {
+ let display = self.sync_display(&display);
+ display.handle_webvr_event(&event);
+ }
+ };
+ }
+
+ fn notify_event(&self, display: &VRDisplay, event: &webvr::VRDisplayEvent) {
+ let event = VRDisplayEvent::new_from_webvr(&self.global(), &display, &event);
+ event.upcast::().fire(self.upcast());
+ }
+}
+
diff --git a/components/script/dom/vrdisplay.rs b/components/script/dom/vrdisplay.rs
new file mode 100644
index 000000000000..bb7d18114646
--- /dev/null
+++ b/components/script/dom/vrdisplay.rs
@@ -0,0 +1,607 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use canvas_traits::CanvasMsg;
+use core::ops::Deref;
+use dom::bindings::callback::ExceptionHandling;
+use dom::bindings::cell::DOMRefCell;
+use dom::bindings::codegen::Bindings::PerformanceBinding::PerformanceBinding::PerformanceMethods;
+use dom::bindings::codegen::Bindings::VRDisplayBinding;
+use dom::bindings::codegen::Bindings::VRDisplayBinding::VRDisplayMethods;
+use dom::bindings::codegen::Bindings::VRDisplayBinding::VREye;
+use dom::bindings::codegen::Bindings::VRLayerBinding::VRLayer;
+use dom::bindings::codegen::Bindings::WindowBinding::FrameRequestCallback;
+use dom::bindings::codegen::Bindings::WindowBinding::WindowBinding::WindowMethods;
+use dom::bindings::inheritance::Castable;
+use dom::bindings::js::{MutNullableJS, MutJS, Root};
+use dom::bindings::num::Finite;
+use dom::bindings::refcounted::Trusted;
+use dom::bindings::reflector::{DomObject, reflect_dom_object};
+use dom::bindings::str::DOMString;
+use dom::event::Event;
+use dom::eventtarget::EventTarget;
+use dom::globalscope::GlobalScope;
+use dom::promise::Promise;
+use dom::vrdisplaycapabilities::VRDisplayCapabilities;
+use dom::vrdisplayevent::VRDisplayEvent;
+use dom::vreyeparameters::VREyeParameters;
+use dom::vrframedata::VRFrameData;
+use dom::vrpose::VRPose;
+use dom::vrstageparameters::VRStageParameters;
+use dom::webglrenderingcontext::WebGLRenderingContext;
+use ipc_channel::ipc;
+use ipc_channel::ipc::{IpcSender, IpcReceiver};
+use js::jsapi::JSContext;
+use script_runtime::CommonScriptMsg;
+use script_runtime::ScriptThreadEventCategory::WebVREvent;
+use script_thread::Runnable;
+use std::cell::Cell;
+use std::mem;
+use std::rc::Rc;
+use std::sync::mpsc;
+use std::thread;
+use webrender_traits::VRCompositorCommand;
+use webvr_traits::{WebVRDisplayData, WebVRDisplayEvent, WebVRFrameData, WebVRLayer, WebVRMsg};
+
+#[dom_struct]
+pub struct VRDisplay {
+ eventtarget: EventTarget,
+ #[ignore_heap_size_of = "Defined in rust-webvr"]
+ display: DOMRefCell,
+ depth_near: Cell,
+ depth_far: Cell,
+ presenting: Cell,
+ left_eye_params: MutJS,
+ right_eye_params: MutJS,
+ capabilities: MutJS,
+ stage_params: MutNullableJS,
+ #[ignore_heap_size_of = "Defined in rust-webvr"]
+ frame_data: DOMRefCell,
+ #[ignore_heap_size_of = "Defined in rust-webvr"]
+ layer: DOMRefCell,
+ layer_ctx: MutNullableJS,
+ #[ignore_heap_size_of = "Defined in rust-webvr"]
+ next_raf_id: Cell,
+ /// List of request animation frame callbacks
+ #[ignore_heap_size_of = "closures are hard"]
+ raf_callback_list: DOMRefCell>)>>,
+ // Compositor VRFrameData synchonization
+ frame_data_status: Cell,
+ #[ignore_heap_size_of = "channels are hard"]
+ frame_data_receiver: DOMRefCell
+ * 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) {
+ /*
+
+
+
+
+ */
+ 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]);
diff --git a/tests/html/webvr/js/third-party/wglu/wglu-debug-geometry.js b/tests/html/webvr/js/third-party/wglu/wglu-debug-geometry.js
new file mode 100644
index 000000000000..3f48e1aad78c
--- /dev/null
+++ b/tests/html/webvr/js/third-party/wglu/wglu-debug-geometry.js
@@ -0,0 +1,270 @@
+/*
+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.
+*/
+
+var WGLUDebugGeometry = (function() {
+
+ "use strict";
+
+ var debugGeomVS = [
+ "uniform mat4 projectionMat;",
+ "uniform mat4 viewMat;",
+ "uniform mat4 modelMat;",
+ "attribute vec3 position;",
+
+ "void main() {",
+ " gl_Position = projectionMat * viewMat * modelMat * vec4( position, 1.0 );",
+ "}",
+ ].join("\n");
+
+ var debugGeomFS = [
+ "precision mediump float;",
+ "uniform vec4 color;",
+
+ "void main() {",
+ " gl_FragColor = color;",
+ "}",
+ ].join("\n");
+
+ var DebugGeometry = function(gl) {
+ this.gl = gl;
+
+ this.projMat = mat4.create();
+ this.viewMat = mat4.create();
+ this.modelMat = mat4.create();
+
+ this.program = new WGLUProgram(gl);
+ this.program.attachShaderSource(debugGeomVS, gl.VERTEX_SHADER);
+ this.program.attachShaderSource(debugGeomFS, gl.FRAGMENT_SHADER);
+ this.program.bindAttribLocation({ position: 0 });
+ this.program.link();
+
+ var verts = [];
+ var indices = [];
+
+ //
+ // Cube Geometry
+ //
+ this.cubeIndexOffset = indices.length;
+
+ var size = 0.5;
+ // Bottom
+ var idx = verts.length / 3.0;
+ indices.push(idx, idx+1, idx+2);
+ indices.push(idx, idx+2, idx+3);
+
+ verts.push(-size, -size, -size);
+ verts.push(+size, -size, -size);
+ verts.push(+size, -size, +size);
+ verts.push(-size, -size, +size);
+
+ // Top
+ idx = verts.length / 3.0;
+ indices.push(idx, idx+2, idx+1);
+ indices.push(idx, idx+3, idx+2);
+
+ verts.push(-size, +size, -size);
+ verts.push(+size, +size, -size);
+ verts.push(+size, +size, +size);
+ verts.push(-size, +size, +size);
+
+ // Left
+ idx = verts.length / 3.0;
+ indices.push(idx, idx+2, idx+1);
+ indices.push(idx, idx+3, idx+2);
+
+ verts.push(-size, -size, -size);
+ verts.push(-size, +size, -size);
+ verts.push(-size, +size, +size);
+ verts.push(-size, -size, +size);
+
+ // Right
+ idx = verts.length / 3.0;
+ indices.push(idx, idx+1, idx+2);
+ indices.push(idx, idx+2, idx+3);
+
+ verts.push(+size, -size, -size);
+ verts.push(+size, +size, -size);
+ verts.push(+size, +size, +size);
+ verts.push(+size, -size, +size);
+
+ // Back
+ idx = verts.length / 3.0;
+ indices.push(idx, idx+2, idx+1);
+ indices.push(idx, idx+3, idx+2);
+
+ verts.push(-size, -size, -size);
+ verts.push(+size, -size, -size);
+ verts.push(+size, +size, -size);
+ verts.push(-size, +size, -size);
+
+ // Front
+ idx = verts.length / 3.0;
+ indices.push(idx, idx+1, idx+2);
+ indices.push(idx, idx+2, idx+3);
+
+ verts.push(-size, -size, +size);
+ verts.push(+size, -size, +size);
+ verts.push(+size, +size, +size);
+ verts.push(-size, +size, +size);
+
+ this.cubeIndexCount = indices.length - this.cubeIndexOffset;
+
+ //
+ // Cone Geometry
+ //
+ this.coneIndexOffset = indices.length;
+
+ var size = 0.5;
+ var conePointVertex = verts.length / 3.0;
+ var coneBaseVertex = conePointVertex+1;
+ var coneSegments = 16;
+
+ // Point
+ verts.push(0, size, 0);
+
+ // Base Vertices
+ for (var i = 0; i < coneSegments; ++i) {
+ if (i > 0) {
+ idx = verts.length / 3.0;
+ indices.push(idx-1, conePointVertex, idx);
+ }
+
+ var rad = ((Math.PI * 2) / coneSegments) * i;
+ verts.push(Math.sin(rad) * (size / 2.0), -size, Math.cos(rad) * (size / 2.0));
+ }
+
+ // Last triangle to fill the gap
+ indices.push(idx, conePointVertex, coneBaseVertex);
+
+ // Base triangles
+ for (var i = 2; i < coneSegments; ++i) {
+ indices.push(coneBaseVertex, coneBaseVertex+(i-1), coneBaseVertex+i);
+ }
+
+ this.coneIndexCount = indices.length - this.coneIndexOffset;
+
+ //
+ // Rect geometry
+ //
+ this.rectIndexOffset = indices.length;
+
+ idx = verts.length / 3.0;
+ indices.push(idx, idx+1, idx+2, idx+3, idx);
+
+ verts.push(0, 0, 0);
+ verts.push(1, 0, 0);
+ verts.push(1, 1, 0);
+ verts.push(0, 1, 0);
+
+ this.rectIndexCount = indices.length - this.rectIndexOffset;
+
+ this.vertBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.STATIC_DRAW);
+
+ this.indexBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
+ };
+
+ DebugGeometry.prototype.bind = function(projectionMat, viewMat) {
+ var gl = this.gl;
+ var program = this.program;
+
+ program.use();
+
+ gl.uniformMatrix4fv(program.uniform.projectionMat, false, projectionMat);
+ gl.uniformMatrix4fv(program.uniform.viewMat, false, viewMat);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
+
+ gl.enableVertexAttribArray(program.attrib.position);
+
+ gl.vertexAttribPointer(program.attrib.position, 3, gl.FLOAT, false, 12, 0);
+ };
+
+ DebugGeometry.prototype.bindOrtho = function() {
+ mat4.ortho(this.projMat, 0, this.gl.canvas.width, this.gl.canvas.height, 0, 0.1, 1024);
+ mat4.identity(this.viewMat);
+ this.bind(this.projMat, this.viewMat);
+ };
+
+ DebugGeometry.prototype._bindUniforms = function(orientation, position, scale, color) {
+ if (!position) { position = [0, 0, 0]; }
+ if (!orientation) { orientation = [0, 0, 0, 1]; }
+ if (!scale) { scale = [1, 1, 1]; }
+ if (!color) { color = [1, 0, 0, 1]; }
+
+ mat4.fromRotationTranslationScale(this.modelMat, orientation, position, scale);
+ this.gl.uniformMatrix4fv(this.program.uniform.modelMat, false, this.modelMat);
+ this.gl.uniform4fv(this.program.uniform.color, color);
+ };
+
+ DebugGeometry.prototype.drawCube = function(orientation, position, size, color) {
+ var gl = this.gl;
+
+ if (!size) { size = 1; }
+ this._bindUniforms(orientation, position, [size, size, size], color);
+ gl.drawElements(gl.TRIANGLES, this.cubeIndexCount, gl.UNSIGNED_SHORT, this.cubeIndexOffset * 2.0);
+ };
+
+ DebugGeometry.prototype.drawBox = function(orientation, position, scale, color) {
+ var gl = this.gl;
+
+ this._bindUniforms(orientation, position, scale, color);
+ gl.drawElements(gl.TRIANGLES, this.cubeIndexCount, gl.UNSIGNED_SHORT, this.cubeIndexOffset * 2.0);
+ };
+
+ DebugGeometry.prototype.drawBoxWithMatrix = function(mat, color) {
+ var gl = this.gl;
+
+ gl.uniformMatrix4fv(this.program.uniform.modelMat, false, mat);
+ gl.uniform4fv(this.program.uniform.color, color);
+ gl.drawElements(gl.TRIANGLES, this.cubeIndexCount, gl.UNSIGNED_SHORT, this.cubeIndexOffset * 2.0);
+ };
+
+ DebugGeometry.prototype.drawRect = function(x, y, width, height, color) {
+ var gl = this.gl;
+
+ this._bindUniforms(null, [x, y, -1], [width, height, 1], color);
+ gl.drawElements(gl.LINE_STRIP, this.rectIndexCount, gl.UNSIGNED_SHORT, this.rectIndexOffset * 2.0);
+ };
+
+ DebugGeometry.prototype.drawCone = function(orientation, position, size, color) {
+ var gl = this.gl;
+
+ if (!size) { size = 1; }
+ this._bindUniforms(orientation, position, [size, size, size], color);
+ gl.drawElements(gl.TRIANGLES, this.coneIndexCount, gl.UNSIGNED_SHORT, this.coneIndexOffset * 2.0);
+ };
+
+ DebugGeometry.prototype.drawConeWithMatrix = function(mat, color) {
+ var gl = this.gl;
+
+ gl.uniformMatrix4fv(this.program.uniform.modelMat, false, mat);
+ gl.uniform4fv(this.program.uniform.color, color);
+ gl.drawElements(gl.TRIANGLES, this.coneIndexCount, gl.UNSIGNED_SHORT, this.coneIndexOffset * 2.0);
+ };
+
+ return DebugGeometry;
+})();
diff --git a/tests/html/webvr/js/third-party/wglu/wglu-preserve-state.js b/tests/html/webvr/js/third-party/wglu/wglu-preserve-state.js
new file mode 100644
index 000000000000..16e1b6049ec1
--- /dev/null
+++ b/tests/html/webvr/js/third-party/wglu/wglu-preserve-state.js
@@ -0,0 +1,162 @@
+/*
+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);
+ }
+ }
+}
diff --git a/tests/html/webvr/js/third-party/wglu/wglu-program.js b/tests/html/webvr/js/third-party/wglu/wglu-program.js
new file mode 100644
index 000000000000..911182edb8f4
--- /dev/null
+++ b/tests/html/webvr/js/third-party/wglu/wglu-program.js
@@ -0,0 +1,179 @@
+/*
+Copyright (c) 2015, 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.
+*/
+
+/*
+Utility class to make loading shader programs easier. Does all the error
+checking you typically want, automatically queries uniform and attribute
+locations, and attempts to take advantage of some browser's ability to link
+asynchronously by not querying any information from the program until it's
+first use.
+*/
+var WGLUProgram = (function() {
+
+ "use strict";
+
+ // Attempts to allow the browser to asynchronously compile and link
+ var Program = function(gl) {
+ this.gl = gl;
+ this.program = gl.createProgram();
+ this.attrib = null;
+ this.uniform = null;
+
+ this._firstUse = true;
+ this._vertexShader = null;
+ this._fragmentShader = null;
+ }
+
+ Program.prototype.attachShaderSource = function(source, type) {
+ var gl = this.gl;
+ var shader;
+
+ switch (type) {
+ case gl.VERTEX_SHADER:
+ this._vertexShader = gl.createShader(type);
+ shader = this._vertexShader;
+ break;
+ case gl.FRAGMENT_SHADER:
+ this._fragmentShader = gl.createShader(type);
+ shader = this._fragmentShader;
+ break;
+ default:
+ console.Error("Invalid Shader Type:", type);
+ return;
+ }
+
+ gl.attachShader(this.program, shader);
+ gl.shaderSource(shader, source);
+ gl.compileShader(shader);
+ }
+
+ Program.prototype.attachShaderSourceFromXHR = function(url, type) {
+ var self = this;
+ return new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.addEventListener("load", function (ev) {
+ if (xhr.status == 200) {
+ self.attachShaderSource(xhr.response, type);
+ resolve();
+ } else {
+ reject(xhr.statusText);
+ }
+ }, false);
+ xhr.open("GET", url, true);
+ xhr.send(null);
+ });
+ }
+
+ Program.prototype.attachShaderSourceFromTag = function(tagId, type) {
+ var shaderTag = document.getElementById(tagId);
+ if (!shaderTag) {
+ console.error("Shader source tag not found:", tagId);
+ return;
+ }
+
+ if (!type) {
+ if (shaderTag.type == "x-shader/x-vertex") {
+ type = this.gl.VERTEX_SHADER;
+ } else if (shaderTag.type == "x-shader/x-fragment") {
+ type = this.gl.FRAGMENT_SHADER;
+ } else {
+ console.error("Invalid Shader Type:", shaderTag.type);
+ return;
+ }
+ }
+
+ var src = "";
+ var k = shaderTag.firstChild;
+ while (k) {
+ if (k.nodeType == 3) {
+ src += k.textContent;
+ }
+ k = k.nextSibling;
+ }
+ this.attachShaderSource(src, type);
+ }
+
+ Program.prototype.bindAttribLocation = function(attribLocationMap) {
+ var gl = this.gl;
+
+ if (attribLocationMap) {
+ this.attrib = {};
+ for (var attribName in attribLocationMap) {
+ gl.bindAttribLocation(this.program, attribLocationMap[attribName], attribName);
+ this.attrib[attribName] = attribLocationMap[attribName];
+ }
+ }
+ }
+
+ Program.prototype.transformFeedbackVaryings = function(varyings, type) {
+ gl.transformFeedbackVaryings(this.program, varyings, type);
+ }
+
+ Program.prototype.link = function() {
+ this.gl.linkProgram(this.program);
+ }
+
+ Program.prototype.use = function() {
+ var gl = this.gl;
+
+ // If this is the first time the program has been used do all the error checking and
+ // attrib/uniform querying needed.
+ if (this._firstUse) {
+ if (!gl.getProgramParameter(this.program, gl.LINK_STATUS)) {
+ if (this._vertexShader && !gl.getShaderParameter(this._vertexShader, gl.COMPILE_STATUS)) {
+ console.error("Vertex shader compile error:", gl.getShaderInfoLog(this._vertexShader));
+ } else if (this._fragmentShader && !gl.getShaderParameter(this._fragmentShader, gl.COMPILE_STATUS)) {
+ console.error("Fragment shader compile error:", gl.getShaderInfoLog(this._fragmentShader));
+ } else {
+ console.error("Program link error:", gl.getProgramInfoLog(this.program));
+ }
+ gl.deleteProgram(this.program);
+ this.program = null;
+ } else {
+ if (!this.attrib) {
+ this.attrib = {};
+ var attribCount = gl.getProgramParameter(this.program, gl.ACTIVE_ATTRIBUTES);
+ for (var i = 0; i < attribCount; i++) {
+ var attribInfo = gl.getActiveAttrib(this.program, i);
+ this.attrib[attribInfo.name] = gl.getAttribLocation(this.program, attribInfo.name);
+ }
+ }
+
+ this.uniform = {};
+ var uniformCount = gl.getProgramParameter(this.program, gl.ACTIVE_UNIFORMS);
+ var uniformName = "";
+ for (var i = 0; i < uniformCount; i++) {
+ var uniformInfo = gl.getActiveUniform(this.program, i);
+ uniformName = uniformInfo.name.replace("[0]", "");
+ this.uniform[uniformName] = gl.getUniformLocation(this.program, uniformName);
+ }
+ }
+ gl.deleteShader(this._vertexShader);
+ gl.deleteShader(this._fragmentShader);
+ this._firstUse = false;
+ }
+
+ gl.useProgram(this.program);
+ }
+
+ return Program;
+})();
diff --git a/tests/html/webvr/js/third-party/wglu/wglu-stats.js b/tests/html/webvr/js/third-party/wglu/wglu-stats.js
new file mode 100644
index 000000000000..70aee00671b5
--- /dev/null
+++ b/tests/html/webvr/js/third-party/wglu/wglu-stats.js
@@ -0,0 +1,649 @@
+/*
+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.
+*/
+
+/*
+Heavily inspired by Mr. Doobs stats.js, this FPS counter is rendered completely
+with WebGL, allowing it to be shown in cases where overlaid HTML elements aren't
+usable (like WebVR), or if you want the FPS counter to be rendered as part of
+your scene.
+
+See stats-test.html for basic usage.
+*/
+var WGLUStats = (function() {
+
+ "use strict";
+
+ //--------------------
+ // glMatrix functions
+ //--------------------
+
+ // These functions have been copied here from glMatrix (glmatrix.net) to allow
+ // this file to run standalone.
+
+ var mat4_identity = function(out) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = 1;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 1;
+ out[11] = 0;
+ out[12] = 0;
+ out[13] = 0;
+ out[14] = 0;
+ out[15] = 1;
+ return out;
+ };
+
+ var mat4_multiply = function (out, a, b) {
+ 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];
+
+ // Cache only the current line of the second matrix
+ var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
+ out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+ b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
+ out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+ b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
+ out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+
+ b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
+ out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30;
+ out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31;
+ out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32;
+ out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33;
+ return out;
+ };
+
+ var mat4_fromTranslation = function(out, v) {
+ out[0] = 1;
+ out[1] = 0;
+ out[2] = 0;
+ out[3] = 0;
+ out[4] = 0;
+ out[5] = 1;
+ out[6] = 0;
+ out[7] = 0;
+ out[8] = 0;
+ out[9] = 0;
+ out[10] = 1;
+ out[11] = 0;
+ out[12] = v[0];
+ out[13] = v[1];
+ out[14] = v[2];
+ out[15] = 1;
+ return out;
+ };
+
+ var mat4_ortho = 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;
+ };
+
+ var mat4_translate = function (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;
+ };
+
+ var mat4_scale = function(out, a, v) {
+ var x = v[0], y = v[1], z = v[2];
+
+ out[0] = a[0] * x;
+ out[1] = a[1] * x;
+ out[2] = a[2] * x;
+ out[3] = a[3] * x;
+ out[4] = a[4] * y;
+ out[5] = a[5] * y;
+ out[6] = a[6] * y;
+ out[7] = a[7] * y;
+ out[8] = a[8] * z;
+ out[9] = a[9] * z;
+ out[10] = a[10] * z;
+ out[11] = a[11] * z;
+ out[12] = a[12];
+ out[13] = a[13];
+ out[14] = a[14];
+ out[15] = a[15];
+ return out;
+ };
+
+ //-------------------
+ // Utility functions
+ //-------------------
+
+ function linkProgram(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;
+ }
+
+ function getProgramUniforms(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;
+ }
+
+ //----------------------------
+ // Seven-segment text display
+ //----------------------------
+
+ var sevenSegmentVS = [
+ "uniform mat4 projectionMat;",
+ "uniform mat4 modelViewMat;",
+ "attribute vec2 position;",
+
+ "void main() {",
+ " gl_Position = projectionMat * modelViewMat * vec4( position, 0.0, 1.0 );",
+ "}",
+ ].join("\n");
+
+ var sevenSegmentFS = [
+ "precision mediump float;",
+ "uniform vec4 color;",
+
+ "void main() {",
+ " gl_FragColor = color;",
+ "}",
+ ].join("\n");
+
+ var SevenSegmentText = function (gl) {
+ this.gl = gl;
+
+ this.attribs = {
+ position: 0,
+ color: 1
+ };
+
+ this.program = linkProgram(gl, sevenSegmentVS, sevenSegmentFS, this.attribs);
+ this.uniforms = getProgramUniforms(gl, this.program);
+
+ var verts = [];
+ var segmentIndices = {};
+ var indices = [];
+
+ var width = 0.5;
+ var thickness = 0.25;
+ this.kerning = 2.0;
+
+ this.matrix = new Float32Array(16);
+
+ function defineSegment(id, left, top, right, bottom) {
+ var idx = verts.length / 2;
+ verts.push(
+ left, top,
+ right, top,
+ right, bottom,
+ left, bottom);
+
+ segmentIndices[id] = [
+ idx, idx+2, idx+1,
+ idx, idx+3, idx+2];
+ }
+
+ var characters = {};
+ this.characters = characters;
+
+ function defineCharacter(c, segments) {
+ var character = {
+ character: c,
+ offset: indices.length * 2,
+ count: 0
+ };
+
+ for (var i = 0; i < segments.length; ++i) {
+ var idx = segments[i];
+ var segment = segmentIndices[idx];
+ character.count += segment.length;
+ indices.push.apply(indices, segment);
+ }
+
+ characters[c] = character;
+ }
+
+ /* Segment layout is as follows:
+
+ |-0-|
+ 3 4
+ |-1-|
+ 5 6
+ |-2-|
+
+ */
+
+ defineSegment(0, -1, 1, width, 1-thickness);
+ defineSegment(1, -1, thickness*0.5, width, -thickness*0.5);
+ defineSegment(2, -1, -1+thickness, width, -1);
+ defineSegment(3, -1, 1, -1+thickness, -thickness*0.5);
+ defineSegment(4, width-thickness, 1, width, -thickness*0.5);
+ defineSegment(5, -1, thickness*0.5, -1+thickness, -1);
+ defineSegment(6, width-thickness, thickness*0.5, width, -1);
+
+
+ defineCharacter("0", [0, 2, 3, 4, 5, 6]);
+ defineCharacter("1", [4, 6]);
+ defineCharacter("2", [0, 1, 2, 4, 5]);
+ defineCharacter("3", [0, 1, 2, 4, 6]);
+ defineCharacter("4", [1, 3, 4, 6]);
+ defineCharacter("5", [0, 1, 2, 3, 6]);
+ defineCharacter("6", [0, 1, 2, 3, 5, 6]);
+ defineCharacter("7", [0, 4, 6]);
+ defineCharacter("8", [0, 1, 2, 3, 4, 5, 6]);
+ defineCharacter("9", [0, 1, 2, 3, 4, 6]);
+ defineCharacter("A", [0, 1, 3, 4, 5, 6]);
+ defineCharacter("B", [1, 2, 3, 5, 6]);
+ defineCharacter("C", [0, 2, 3, 5]);
+ defineCharacter("D", [1, 2, 4, 5, 6]);
+ defineCharacter("E", [0, 1, 2, 4, 6]);
+ defineCharacter("F", [0, 1, 3, 5]);
+ defineCharacter("P", [0, 1, 3, 4, 5]);
+ defineCharacter("-", [1]);
+ defineCharacter(" ", []);
+ defineCharacter("_", [2]); // Used for undefined characters
+
+ this.vertBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verts), gl.DYNAMIC_DRAW);
+
+ this.indexBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
+ };
+
+ SevenSegmentText.prototype.render = function(projectionMat, modelViewMat, text, r, g, b, a) {
+ var gl = this.gl;
+
+ if (r == undefined || g == undefined || b == undefined) {
+ r = 0.0;
+ g = 1.0;
+ b = 0.0;
+ }
+
+ if (a == undefined)
+ a = 1.0;
+
+ gl.useProgram(this.program);
+
+ gl.uniformMatrix4fv(this.uniforms.projectionMat, false, projectionMat);
+ gl.uniform4f(this.uniforms.color, r, g, b, a);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
+
+ gl.enableVertexAttribArray(this.attribs.position);
+ gl.vertexAttribPointer(this.attribs.position, 2, gl.FLOAT, false, 8, 0);
+
+ text = text.toUpperCase();
+
+ var offset = 0;
+
+ for (var i = 0; i < text.length; ++i) {
+ var c;
+ if (text[i] in this.characters) {
+ c = this.characters[text[i]];
+ } else {
+ c = this.characters["_"];
+ }
+
+ if (c.count != 0) {
+ mat4_fromTranslation(this.matrix, [offset, 0, 0]);
+ mat4_multiply(this.matrix, modelViewMat, this.matrix);
+
+ gl.uniformMatrix4fv(this.uniforms.modelViewMat, false, this.matrix);
+ gl.drawElements(gl.TRIANGLES, c.count, gl.UNSIGNED_SHORT, c.offset);
+
+ }
+
+ offset += this.kerning;
+ }
+ }
+
+ //-----------
+ // FPS Graph
+ //-----------
+
+ var statsVS = [
+ "uniform mat4 projectionMat;",
+ "uniform mat4 modelViewMat;",
+ "attribute vec3 position;",
+ "attribute vec3 color;",
+ "varying vec4 vColor;",
+
+ "void main() {",
+ " vColor = vec4(color, 1.0);",
+ " gl_Position = projectionMat * modelViewMat * vec4( position, 1.0 );",
+ "}",
+ ].join("\n");
+
+ var statsFS = [
+ "precision mediump float;",
+ "varying vec4 vColor;",
+
+ "void main() {",
+ " gl_FragColor = vColor;",
+ "}",
+ ].join("\n");
+
+ var segments = 30;
+ var maxFPS = 90;
+
+ function segmentToX(i) {
+ return ((0.9/segments) * i) - 0.45;
+ }
+
+ function fpsToY(value) {
+ return (Math.min(value, maxFPS) * (0.7 / maxFPS)) - 0.45;
+ }
+
+ function fpsToRGB(value) {
+ return {
+ r: Math.max(0.0, Math.min(1.0, 1.0 - (value/60))),
+ g: Math.max(0.0, Math.min(1.0, ((value-15)/(maxFPS-15)))),
+ b: Math.max(0.0, Math.min(1.0, ((value-15)/(maxFPS-15))))
+ };
+ }
+
+ var now = /*( performance && performance.now ) ? performance.now.bind( performance ) :*/ Date.now;
+
+ var Stats = function(gl) {
+ this.gl = gl;
+
+ this.sevenSegmentText = new SevenSegmentText(gl);
+
+ this.startTime = now();
+ this.prevTime = this.startTime;
+ this.frames = 0;
+ this.fps = 0;
+
+ this.orthoProjMatrix = new Float32Array(16);
+ this.orthoViewMatrix = new Float32Array(16);
+ this.modelViewMatrix = new Float32Array(16);
+
+ // Hard coded because it doesn't change:
+ // Scale by 0.075 in X and Y
+ // Translate into upper left corner w/ z = 0.02
+ this.textMatrix = new Float32Array([
+ 0.075, 0, 0, 0,
+ 0, 0.075, 0, 0,
+ 0, 0, 1, 0,
+ -0.3625, 0.3625, 0.02, 1
+ ]);
+
+ this.lastSegment = 0;
+
+ this.attribs = {
+ position: 0,
+ color: 1
+ };
+
+ this.program = linkProgram(gl, statsVS, statsFS, this.attribs);
+ this.uniforms = getProgramUniforms(gl, this.program);
+
+ var fpsVerts = [];
+ var fpsIndices = [];
+
+ // Graph geometry
+ for (var i = 0; i < segments; ++i) {
+ // Bar top
+ fpsVerts.push(segmentToX(i), fpsToY(0), 0.02, 0.0, 1.0, 1.0);
+ fpsVerts.push(segmentToX(i+1), fpsToY(0), 0.02, 0.0, 1.0, 1.0);
+
+ // Bar bottom
+ fpsVerts.push(segmentToX(i), fpsToY(0), 0.02, 0.0, 1.0, 1.0);
+ fpsVerts.push(segmentToX(i+1), fpsToY(0), 0.02, 0.0, 1.0, 1.0);
+
+ var idx = i * 4;
+ fpsIndices.push(idx, idx+3, idx+1,
+ idx+3, idx, idx+2);
+ }
+
+ function addBGSquare(left, bottom, right, top, z, r, g, b) {
+ var idx = fpsVerts.length / 6;
+
+ fpsVerts.push(left, bottom, z, r, g, b);
+ fpsVerts.push(right, top, z, r, g, b);
+ fpsVerts.push(left, top, z, r, g, b);
+ fpsVerts.push(right, bottom, z, r, g, b);
+
+ fpsIndices.push(idx, idx+1, idx+2,
+ idx, idx+3, idx+1);
+ };
+
+ // Panel Background
+ addBGSquare(-0.5, -0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.125);
+
+ // FPS Background
+ addBGSquare(-0.45, -0.45, 0.45, 0.25, 0.01, 0.0, 0.0, 0.4);
+
+ // 30 FPS line
+ addBGSquare(-0.45, fpsToY(30), 0.45, fpsToY(32), 0.015, 0.5, 0.0, 0.5);
+
+ // 60 FPS line
+ addBGSquare(-0.45, fpsToY(60), 0.45, fpsToY(62), 0.015, 0.2, 0.0, 0.75);
+
+ this.fpsVertBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.fpsVertBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(fpsVerts), gl.DYNAMIC_DRAW);
+
+ this.fpsIndexBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.fpsIndexBuffer);
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(fpsIndices), gl.STATIC_DRAW);
+
+ this.fpsIndexCount = fpsIndices.length;
+ };
+
+ Stats.prototype.begin = function() {
+ this.startTime = now();
+ };
+
+ Stats.prototype.end = function() {
+ var time = now();
+
+ this.frames++;
+
+ if (time > this.prevTime + 250) {
+ this.fps = Math.round((this.frames * 1000) / (time - this.prevTime));
+
+ this.updateGraph(this.fps);
+
+ this.prevTime = time;
+ this.frames = 0;
+ }
+ };
+
+ Stats.prototype.updateGraph = function(value) {
+ var gl = this.gl;
+
+ var color = fpsToRGB(value);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.fpsVertBuffer);
+
+ // Update the current segment with the new FPS value
+ var updateVerts = [
+ segmentToX(this.lastSegment), fpsToY(value), 0.02, color.r, color.g, color.b,
+ segmentToX(this.lastSegment+1), fpsToY(value), 0.02, color.r, color.g, color.b,
+ segmentToX(this.lastSegment), fpsToY(0), 0.02, color.r, color.g, color.b,
+ segmentToX(this.lastSegment+1), fpsToY(0), 0.02, color.r, color.g, color.b,
+ ];
+
+ // Re-shape the next segment into the green "progress" line
+ color.r = 0.2;
+ color.g = 1.0;
+ color.b = 0.2;
+
+ if (this.lastSegment == segments - 1) {
+ // If we're updating the last segment we need to do two bufferSubDatas
+ // to update the segment and turn the first segment into the progress line.
+ gl.bufferSubData(gl.ARRAY_BUFFER, this.lastSegment * 24 * 4, new Float32Array(updateVerts));
+ updateVerts = [
+ segmentToX(0), fpsToY(maxFPS), 0.02, color.r, color.g, color.b,
+ segmentToX(.25), fpsToY(maxFPS), 0.02, color.r, color.g, color.b,
+ segmentToX(0), fpsToY(0), 0.02, color.r, color.g, color.b,
+ segmentToX(.25), fpsToY(0), 0.02, color.r, color.g, color.b
+ ];
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(updateVerts));
+ } else {
+ updateVerts.push(
+ segmentToX(this.lastSegment+1), fpsToY(maxFPS), 0.02, color.r, color.g, color.b,
+ segmentToX(this.lastSegment+1.25), fpsToY(maxFPS), 0.02, color.r, color.g, color.b,
+ segmentToX(this.lastSegment+1), fpsToY(0), 0.02, color.r, color.g, color.b,
+ segmentToX(this.lastSegment+1.25), fpsToY(0), 0.02, color.r, color.g, color.b
+ );
+ gl.bufferSubData(gl.ARRAY_BUFFER, this.lastSegment * 24 * 4, new Float32Array(updateVerts));
+ }
+
+ this.lastSegment = (this.lastSegment+1) % segments;
+ };
+
+ Stats.prototype.render = function(projectionMat, modelViewMat) {
+ var gl = this.gl;
+
+ // Render text first, minor win for early fragment discard
+ mat4_multiply(this.modelViewMatrix, modelViewMat, this.textMatrix);
+ this.sevenSegmentText.render(projectionMat, this.modelViewMatrix, this.fps + " FP5");
+
+ gl.useProgram(this.program);
+
+ gl.uniformMatrix4fv(this.uniforms.projectionMat, false, projectionMat);
+ gl.uniformMatrix4fv(this.uniforms.modelViewMat, false, modelViewMat);
+
+ gl.enableVertexAttribArray(this.attribs.position);
+ gl.enableVertexAttribArray(this.attribs.color);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.fpsVertBuffer);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.fpsIndexBuffer);
+
+ gl.vertexAttribPointer(this.attribs.position, 3, gl.FLOAT, false, 24, 0);
+ gl.vertexAttribPointer(this.attribs.color, 3, gl.FLOAT, false, 24, 12);
+
+ // Draw the graph and background in a single call
+ gl.drawElements(gl.TRIANGLES, this.fpsIndexCount, gl.UNSIGNED_SHORT, 0);
+ }
+
+ Stats.prototype.renderOrtho = function(x, y, width, height) {
+ var canvas = this.gl.canvas;
+
+ if (x == undefined || y == undefined) {
+ x = 10 * window.devicePixelRatio;
+ y = 10 * window.devicePixelRatio;
+ }
+ if (width == undefined || height == undefined) {
+ width = 75 * window.devicePixelRatio;
+ height = 75 * window.devicePixelRatio;
+ }
+
+ mat4_ortho(this.orthoProjMatrix, 0, canvas.width, 0, canvas.height, 0.1, 1024);
+
+ mat4_identity(this.orthoViewMatrix);
+ mat4_translate(this.orthoViewMatrix, this.orthoViewMatrix, [x, canvas.height - height - y, -1]);
+ mat4_scale(this.orthoViewMatrix, this.orthoViewMatrix, [width, height, 1]);
+ mat4_translate(this.orthoViewMatrix, this.orthoViewMatrix, [0.5, 0.5, 0]);
+
+ this.render(this.orthoProjMatrix, this.orthoViewMatrix);
+ }
+
+ return Stats;
+})();
diff --git a/tests/html/webvr/js/third-party/wglu/wglu-texture.js b/tests/html/webvr/js/third-party/wglu/wglu-texture.js
new file mode 100644
index 000000000000..6bfa368ab92c
--- /dev/null
+++ b/tests/html/webvr/js/third-party/wglu/wglu-texture.js
@@ -0,0 +1,687 @@
+/*
+Copyright (c) 2015, 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.
+*/
+
+/*
+Handles loading of textures of mutliple formats, tries to be efficent about it.
+
+Formats supported will vary by devices. Use the .supports() functions
+to determine if a format is supported. Most of the time you can just call
+loader.loadTexture("url"); and it will handle it based on the extension.
+If the extension can't be relied on use the corresponding
+.load("url") calls.
+*/
+var WGLUTextureLoader = (function() {
+
+ "use strict";
+
+ //============================//
+ // DXT constants and utilites //
+ //============================//
+
+ // Utility functions
+ // Builds a numeric code for a given fourCC string
+ function fourCCToInt32(value) {
+ return value.charCodeAt(0) +
+ (value.charCodeAt(1) << 8) +
+ (value.charCodeAt(2) << 16) +
+ (value.charCodeAt(3) << 24);
+ }
+
+ // Turns a fourCC numeric code into a string
+ function int32ToFourCC(value) {
+ return String.fromCharCode(
+ value & 0xff,
+ (value >> 8) & 0xff,
+ (value >> 16) & 0xff,
+ (value >> 24) & 0xff
+ );
+ }
+
+ // Calcualates the size of a compressed texture level in bytes
+ function textureLevelSize(format, width, height) {
+ switch (format) {
+ case COMPRESSED_RGB_S3TC_DXT1_EXT:
+ case COMPRESSED_RGB_ATC_WEBGL:
+ case COMPRESSED_RGB_ETC1_WEBGL:
+ return ((width + 3) >> 2) * ((height + 3) >> 2) * 8;
+
+ case COMPRESSED_RGBA_S3TC_DXT3_EXT:
+ case COMPRESSED_RGBA_S3TC_DXT5_EXT:
+ case COMPRESSED_RGBA_ATC_EXPLICIT_ALPHA_WEBGL:
+ case COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL:
+ return ((width + 3) >> 2) * ((height + 3) >> 2) * 16;
+
+ case COMPRESSED_RGB_PVRTC_4BPPV1_IMG:
+ case COMPRESSED_RGBA_PVRTC_4BPPV1_IMG:
+ return Math.floor((Math.max(width, 8) * Math.max(height, 8) * 4 + 7) / 8);
+
+ case COMPRESSED_RGB_PVRTC_2BPPV1_IMG:
+ case COMPRESSED_RGBA_PVRTC_2BPPV1_IMG:
+ return Math.floor((Math.max(width, 16) * Math.max(height, 8) * 2 + 7) / 8);
+
+ default:
+ return 0;
+ }
+ }
+
+ // DXT formats, from:
+ // http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
+ var COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0;
+ var COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1;
+ var COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2;
+ var COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3;
+
+ // ATC formats, from:
+ // http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_atc/
+ var COMPRESSED_RGB_ATC_WEBGL = 0x8C92;
+ var COMPRESSED_RGBA_ATC_EXPLICIT_ALPHA_WEBGL = 0x8C93;
+ var COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL = 0x87EE;
+
+ // DXT values and structures referenced from:
+ // http://msdn.microsoft.com/en-us/library/bb943991.aspx/
+ var DDS_MAGIC = 0x20534444;
+ var DDSD_MIPMAPCOUNT = 0x20000;
+ var DDPF_FOURCC = 0x4;
+
+ var DDS_HEADER_LENGTH = 31; // The header length in 32 bit ints.
+
+ // Offsets into the header array.
+ var DDS_HEADER_MAGIC = 0;
+
+ var DDS_HEADER_SIZE = 1;
+ var DDS_HEADER_FLAGS = 2;
+ var DDS_HEADER_HEIGHT = 3;
+ var DDS_HEADER_WIDTH = 4;
+
+ var DDS_HEADER_MIPMAPCOUNT = 7;
+
+ var DDS_HEADER_PF_FLAGS = 20;
+ var DDS_HEADER_PF_FOURCC = 21;
+
+ // FourCC format identifiers.
+ var FOURCC_DXT1 = fourCCToInt32("DXT1");
+ var FOURCC_DXT3 = fourCCToInt32("DXT3");
+ var FOURCC_DXT5 = fourCCToInt32("DXT5");
+
+ var FOURCC_ATC = fourCCToInt32("ATC ");
+ var FOURCC_ATCA = fourCCToInt32("ATCA");
+ var FOURCC_ATCI = fourCCToInt32("ATCI");
+
+ //==================//
+ // Crunch constants //
+ //==================//
+
+ // Taken from crnlib.h
+ var CRN_FORMAT = {
+ cCRNFmtInvalid: -1,
+
+ cCRNFmtDXT1: 0,
+ // cCRNFmtDXT3 is not currently supported when writing to CRN - only DDS.
+ cCRNFmtDXT3: 1,
+ cCRNFmtDXT5: 2
+
+ // Crunch supports more formats than this, but we can't use them here.
+ };
+
+ // Mapping of Crunch formats to DXT formats.
+ var DXT_FORMAT_MAP = {};
+ DXT_FORMAT_MAP[CRN_FORMAT.cCRNFmtDXT1] = COMPRESSED_RGB_S3TC_DXT1_EXT;
+ DXT_FORMAT_MAP[CRN_FORMAT.cCRNFmtDXT3] = COMPRESSED_RGBA_S3TC_DXT3_EXT;
+ DXT_FORMAT_MAP[CRN_FORMAT.cCRNFmtDXT5] = COMPRESSED_RGBA_S3TC_DXT5_EXT;
+
+ //===============//
+ // PVR constants //
+ //===============//
+
+ // PVR formats, from:
+ // http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_pvrtc/
+ var COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00;
+ var COMPRESSED_RGB_PVRTC_2BPPV1_IMG = 0x8C01;
+ var COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02;
+ var COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8C03;
+
+ // ETC1 format, from:
+ // http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_etc1/
+ var COMPRESSED_RGB_ETC1_WEBGL = 0x8D64;
+
+ var PVR_FORMAT_2BPP_RGB = 0;
+ var PVR_FORMAT_2BPP_RGBA = 1;
+ var PVR_FORMAT_4BPP_RGB = 2;
+ var PVR_FORMAT_4BPP_RGBA = 3;
+ var PVR_FORMAT_ETC1 = 6;
+ var PVR_FORMAT_DXT1 = 7;
+ var PVR_FORMAT_DXT3 = 9;
+ var PVR_FORMAT_DXT5 = 5;
+
+ var PVR_HEADER_LENGTH = 13; // The header length in 32 bit ints.
+ var PVR_MAGIC = 0x03525650; //0x50565203;
+
+ // Offsets into the header array.
+ var PVR_HEADER_MAGIC = 0;
+ var PVR_HEADER_FORMAT = 2;
+ var PVR_HEADER_HEIGHT = 6;
+ var PVR_HEADER_WIDTH = 7;
+ var PVR_HEADER_MIPMAPCOUNT = 11;
+ var PVR_HEADER_METADATA = 12;
+
+ //============//
+ // Misc Utils //
+ //============//
+
+ // When an error occurs set the texture to a 1x1 black pixel
+ // This prevents WebGL errors from attempting to use unrenderable textures
+ // and clears out stale data if we're re-using a texture.
+ function clearOnError(gl, error, texture, callback) {
+ if (console) {
+ console.error(error);
+ }
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0]));
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+
+ // Notify the user that an error occurred and the texture is ready.
+ if (callback) { callback(texture, error, null); }
+ }
+
+ function isPowerOfTwo(n) {
+ return (n & (n - 1)) === 0;
+ }
+
+ function getExtension(gl, name) {
+ var vendorPrefixes = ["", "WEBKIT_", "MOZ_"];
+ var ext = null;
+ for (var i in vendorPrefixes) {
+ ext = gl.getExtension(vendorPrefixes[i] + name);
+ if (ext) { break; }
+ }
+ return ext;
+ }
+
+ //==================//
+ // DDS File Reading //
+ //==================//
+
+ // Parse a DDS file and provide information about the raw DXT data it contains to the given callback.
+ function parseDDS(arrayBuffer, callback, errorCallback) {
+ // Callbacks must be provided.
+ if (!callback || !errorCallback) { return; }
+
+ // Get a view of the arrayBuffer that represents the DDS header.
+ var header = new Int32Array(arrayBuffer, 0, DDS_HEADER_LENGTH);
+
+ // Do some sanity checks to make sure this is a valid DDS file.
+ if(header[DDS_HEADER_MAGIC] != DDS_MAGIC) {
+ errorCallback("Invalid magic number in DDS header");
+ return 0;
+ }
+
+ if(!header[DDS_HEADER_PF_FLAGS] & DDPF_FOURCC) {
+ errorCallback("Unsupported format, must contain a FourCC code");
+ return 0;
+ }
+
+ // Determine what type of compressed data the file contains.
+ var fourCC = header[DDS_HEADER_PF_FOURCC];
+ var internalFormat;
+ switch(fourCC) {
+ case FOURCC_DXT1:
+ internalFormat = COMPRESSED_RGB_S3TC_DXT1_EXT;
+ break;
+
+ case FOURCC_DXT3:
+ internalFormat = COMPRESSED_RGBA_S3TC_DXT3_EXT;
+ break;
+
+ case FOURCC_DXT5:
+ internalFormat = COMPRESSED_RGBA_S3TC_DXT5_EXT;
+ break;
+
+ case FOURCC_ATC:
+ internalFormat = COMPRESSED_RGB_ATC_WEBGL;
+ break;
+
+ case FOURCC_ATCA:
+ internalFormat = COMPRESSED_RGBA_ATC_EXPLICIT_ALPHA_WEBGL;
+ break;
+
+ case FOURCC_ATCI:
+ internalFormat = COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL;
+ break;
+
+
+ default:
+ errorCallback("Unsupported FourCC code: " + int32ToFourCC(fourCC));
+ return;
+ }
+
+ // Determine how many mipmap levels the file contains.
+ var levels = 1;
+ if(header[DDS_HEADER_FLAGS] & DDSD_MIPMAPCOUNT) {
+ levels = Math.max(1, header[DDS_HEADER_MIPMAPCOUNT]);
+ }
+
+ // Gather other basic metrics and a view of the raw the DXT data.
+ var width = header[DDS_HEADER_WIDTH];
+ var height = header[DDS_HEADER_HEIGHT];
+ var dataOffset = header[DDS_HEADER_SIZE] + 4;
+ var dxtData = new Uint8Array(arrayBuffer, dataOffset);
+
+ // Pass the DXT information to the callback for uploading.
+ callback(dxtData, width, height, levels, internalFormat);
+ }
+
+ //==================//
+ // PVR File Reading //
+ //==================//
+
+ // Parse a PVR file and provide information about the raw texture data it contains to the given callback.
+ function parsePVR(arrayBuffer, callback, errorCallback) {
+ // Callbacks must be provided.
+ if (!callback || !errorCallback) { return; }
+
+ // Get a view of the arrayBuffer that represents the DDS header.
+ var header = new Int32Array(arrayBuffer, 0, PVR_HEADER_LENGTH);
+
+ // Do some sanity checks to make sure this is a valid DDS file.
+ if(header[PVR_HEADER_MAGIC] != PVR_MAGIC) {
+ errorCallback("Invalid magic number in PVR header");
+ return 0;
+ }
+
+ // Determine what type of compressed data the file contains.
+ var format = header[PVR_HEADER_FORMAT];
+ var internalFormat;
+ switch(format) {
+ case PVR_FORMAT_2BPP_RGB:
+ internalFormat = COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
+ break;
+
+ case PVR_FORMAT_2BPP_RGBA:
+ internalFormat = COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
+ break;
+
+ case PVR_FORMAT_4BPP_RGB:
+ internalFormat = COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
+ break;
+
+ case PVR_FORMAT_4BPP_RGBA:
+ internalFormat = COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
+ break;
+
+ case PVR_FORMAT_ETC1:
+ internalFormat = COMPRESSED_RGB_ETC1_WEBGL;
+ break;
+
+ case PVR_FORMAT_DXT1:
+ internalFormat = COMPRESSED_RGB_S3TC_DXT1_EXT;
+ break;
+
+ case PVR_FORMAT_DXT3:
+ internalFormat = COMPRESSED_RGBA_S3TC_DXT3_EXT;
+ break;
+
+ case PVR_FORMAT_DXT5:
+ internalFormat = COMPRESSED_RGBA_S3TC_DXT5_EXT;
+ break;
+
+ default:
+ errorCallback("Unsupported PVR format: " + format);
+ return;
+ }
+
+ // Gather other basic metrics and a view of the raw the DXT data.
+ var width = header[PVR_HEADER_WIDTH];
+ var height = header[PVR_HEADER_HEIGHT];
+ var levels = header[PVR_HEADER_MIPMAPCOUNT];
+ var dataOffset = header[PVR_HEADER_METADATA] + 52;
+ var pvrtcData = new Uint8Array(arrayBuffer, dataOffset);
+
+ // Pass the PVRTC information to the callback for uploading.
+ callback(pvrtcData, width, height, levels, internalFormat);
+ }
+
+ //=============//
+ // IMG loading //
+ //=============//
+
+ /*
+ This function provides a method for loading webgl textures using a pool of
+ image elements, which has very low memory overhead. For more details see:
+ http://blog.tojicode.com/2012/03/javascript-memory-optimization-and.html
+ */
+ var loadImgTexture = (function createTextureLoader() {
+ var MAX_CACHE_IMAGES = 16;
+
+ var textureImageCache = new Array(MAX_CACHE_IMAGES);
+ var cacheTop = 0;
+ var remainingCacheImages = MAX_CACHE_IMAGES;
+ var pendingTextureRequests = [];
+
+ var TextureImageLoader = function(loadedCallback) {
+ var self = this;
+ var blackPixel = new Uint8Array([0, 0, 0]);
+
+ this.gl = null;
+ this.texture = null;
+ this.callback = null;
+
+ this.image = new Image();
+ this.image.crossOrigin = 'anonymous';
+ this.image.addEventListener('load', function() {
+ var gl = self.gl;
+ gl.bindTexture(gl.TEXTURE_2D, self.texture);
+
+ var startTime = Date.now();
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, self.image);
+
+ if (isPowerOfTwo(self.image.width) && isPowerOfTwo(self.image.height)) {
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
+ gl.generateMipmap(gl.TEXTURE_2D);
+ } else {
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ }
+ var uploadTime = Date.now() - startTime;
+
+ if(self.callback) {
+ var stats = {
+ width: self.image.width,
+ height: self.image.height,
+ internalFormat: gl.RGBA,
+ levelZeroSize: self.image.width * self.image.height * 4,
+ uploadTime: uploadTime
+ };
+ self.callback(self.texture, null, stats);
+ }
+ loadedCallback(self);
+ }, false);
+ this.image.addEventListener('error', function(ev) {
+ clearOnError(self.gl, 'Image could not be loaded: ' + self.image.src, self.texture, self.callback);
+ loadedCallback(self);
+ }, false);
+ };
+
+ TextureImageLoader.prototype.loadTexture = function(gl, src, texture, callback) {
+ this.gl = gl;
+ this.texture = texture;
+ this.callback = callback;
+ this.image.src = src;
+ };
+
+ var PendingTextureRequest = function(gl, src, texture, callback) {
+ this.gl = gl;
+ this.src = src;
+ this.texture = texture;
+ this.callback = callback;
+ };
+
+ function releaseTextureImageLoader(til) {
+ var req;
+ if(pendingTextureRequests.length) {
+ req = pendingTextureRequests.shift();
+ til.loadTexture(req.gl, req.src, req.texture, req.callback);
+ } else {
+ textureImageCache[cacheTop++] = til;
+ }
+ }
+
+ return function(gl, src, texture, callback) {
+ var til;
+
+ if(cacheTop) {
+ til = textureImageCache[--cacheTop];
+ til.loadTexture(gl, src, texture, callback);
+ } else if (remainingCacheImages) {
+ til = new TextureImageLoader(releaseTextureImageLoader);
+ til.loadTexture(gl, src, texture, callback);
+ --remainingCacheImages;
+ } else {
+ pendingTextureRequests.push(new PendingTextureRequest(gl, src, texture, callback));
+ }
+
+ return texture;
+ };
+ })();
+
+ //=====================//
+ // TextureLoader Class //
+ //=====================//
+
+ // This class is our public interface.
+ var TextureLoader = function(gl) {
+ this.gl = gl;
+
+ // Load the compression format extensions, if available
+ this.dxtExt = getExtension(gl, "WEBGL_compressed_texture_s3tc");
+ this.pvrtcExt = getExtension(gl, "WEBGL_compressed_texture_pvrtc");
+ this.atcExt = getExtension(gl, "WEBGL_compressed_texture_atc");
+ this.etc1Ext = getExtension(gl, "WEBGL_compressed_texture_etc1");
+
+ // Returns whether or not the compressed format is supported by the WebGL implementation
+ TextureLoader.prototype._formatSupported = function(format) {
+ switch (format) {
+ case COMPRESSED_RGB_S3TC_DXT1_EXT:
+ case COMPRESSED_RGBA_S3TC_DXT3_EXT:
+ case COMPRESSED_RGBA_S3TC_DXT5_EXT:
+ return !!this.dxtExt;
+
+ case COMPRESSED_RGB_PVRTC_4BPPV1_IMG:
+ case COMPRESSED_RGBA_PVRTC_4BPPV1_IMG:
+ case COMPRESSED_RGB_PVRTC_2BPPV1_IMG:
+ case COMPRESSED_RGBA_PVRTC_2BPPV1_IMG:
+ return !!this.pvrtcExt;
+
+ case COMPRESSED_RGB_ATC_WEBGL:
+ case COMPRESSED_RGBA_ATC_EXPLICIT_ALPHA_WEBGL:
+ case COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL:
+ return !!this.atcExt;
+
+ case COMPRESSED_RGB_ETC1_WEBGL:
+ return !!this.etc1Ext;
+
+ default:
+ return false;
+ }
+ }
+
+ // Uploads compressed texture data to the GPU.
+ TextureLoader.prototype._uploadCompressedData = function(data, width, height, levels, internalFormat, texture, callback) {
+ var gl = this.gl;
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+
+ var offset = 0;
+
+ var stats = {
+ width: width,
+ height: height,
+ internalFormat: internalFormat,
+ levelZeroSize: textureLevelSize(internalFormat, width, height),
+ uploadTime: 0
+ };
+
+ var startTime = Date.now();
+ // Loop through each mip level of compressed texture data provided and upload it to the given texture.
+ for (var i = 0; i < levels; ++i) {
+ // Determine how big this level of compressed texture data is in bytes.
+ var levelSize = textureLevelSize(internalFormat, width, height);
+ // Get a view of the bytes for this level of DXT data.
+ var dxtLevel = new Uint8Array(data.buffer, data.byteOffset + offset, levelSize);
+ // Upload!
+ gl.compressedTexImage2D(gl.TEXTURE_2D, i, internalFormat, width, height, 0, dxtLevel);
+ // The next mip level will be half the height and width of this one.
+ width = width >> 1;
+ height = height >> 1;
+ // Advance the offset into the compressed texture data past the current mip level's data.
+ offset += levelSize;
+ }
+ stats.uploadTime = Date.now() - startTime;
+
+ // We can't use gl.generateMipmaps with compressed textures, so only use
+ // mipmapped filtering if the compressed texture data contained mip levels.
+ if (levels > 1) {
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST);
+ } else {
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+ }
+
+ // Notify the user that the texture is ready.
+ if (callback) { callback(texture, null, stats); }
+ }
+
+ TextureLoader.prototype.supportsDXT = function() {
+ return !!this.dxtExt;
+ }
+
+ TextureLoader.prototype.supportsPVRTC = function() {
+ return !!this.pvrtcExt;
+ }
+
+ TextureLoader.prototype.supportsATC = function() {
+ return !!this.atcExt;
+ }
+
+ TextureLoader.prototype.supportsETC1 = function() {
+ return !!this.etc1Ext;
+ }
+
+ // Loads a image file into the given texture.
+ // Supports any format that can be loaded into an img tag
+ // If no texture is provided one is created and returned.
+ TextureLoader.prototype.loadIMG = function(src, texture, callback) {
+ if(!texture) {
+ texture = this.gl.createTexture();
+ }
+
+ loadImgTexture(gl, src, texture, callback);
+
+ return texture;
+ }
+
+ // Loads a DDS file into the given texture.
+ // If no texture is provided one is created and returned.
+ TextureLoader.prototype.loadDDS = function(src, texture, callback) {
+ var self = this;
+ if (!texture) {
+ texture = this.gl.createTexture();
+ }
+
+ // Load the file via XHR.
+ var xhr = new XMLHttpRequest();
+ xhr.addEventListener('load', function (ev) {
+ if (xhr.status == 200) {
+ // If the file loaded successfully parse it.
+ parseDDS(xhr.response, function(dxtData, width, height, levels, internalFormat) {
+ if (!self._formatSupported(internalFormat)) {
+ clearOnError(self.gl, "Texture format not supported", texture, callback);
+ return;
+ }
+ // Upload the parsed DXT data to the texture.
+ self._uploadCompressedData(dxtData, width, height, levels, internalFormat, texture, callback);
+ }, function(error) {
+ clearOnError(self.gl, error, texture, callback);
+ });
+ } else {
+ clearOnError(self.gl, xhr.statusText, texture, callback);
+ }
+ }, false);
+ xhr.open('GET', src, true);
+ xhr.responseType = 'arraybuffer';
+ xhr.send(null);
+
+ return texture;
+ }
+
+ // Loads a PVR file into the given texture.
+ // If no texture is provided one is created and returned.
+ TextureLoader.prototype.loadPVR = function(src, texture, callback) {
+ var self = this;
+ if(!texture) {
+ texture = this.gl.createTexture();
+ }
+
+ // Load the file via XHR.
+ var xhr = new XMLHttpRequest();
+ xhr.addEventListener('load', function (ev) {
+ if (xhr.status == 200) {
+ // If the file loaded successfully parse it.
+ parsePVR(xhr.response, function(dxtData, width, height, levels, internalFormat) {
+ if (!self._formatSupported(internalFormat)) {
+ clearOnError(self.gl, "Texture format not supported", texture, callback);
+ return;
+ }
+ // Upload the parsed PVR data to the texture.
+ self._uploadCompressedData(dxtData, width, height, levels, internalFormat, texture, callback);
+ }, function(error) {
+ clearOnError(self.gl, error, texture, callback);
+ });
+ } else {
+ clearOnError(self.gl, xhr.statusText, texture, callback);
+ }
+ }, false);
+ xhr.open('GET', src, true);
+ xhr.responseType = 'arraybuffer';
+ xhr.send(null);
+
+ return texture;
+ }
+
+ // Loads a texture from a file. Guesses the type based on extension.
+ // If no texture is provided one is created and returned.
+ TextureLoader.prototype.loadTexture = function(src, texture, callback) {
+ // Shamelessly lifted from StackOverflow :)
+ // http://stackoverflow.com/questions/680929
+ var re = /(?:\.([^.]+))?$/;
+ var ext = re.exec(src)[1] || '';
+ ext = ext.toLowerCase();
+
+ switch(ext) {
+ case 'dds':
+ return this.loadDDS(src, texture, callback);
+ case 'pvr':
+ return this.loadPVR(src, texture, callback);
+ default:
+ return this.loadIMG(src, texture, callback);
+ }
+ }
+
+ // Sets a texture to a solid RGBA color
+ // If no texture is provided one is created and returned.
+ TextureLoader.prototype.makeSolidColor = function(r, g, b, a, texture) {
+ var gl = this.gl;
+ var data = new Uint8Array([r, g, b, a]);
+ if(!texture) {
+ texture = gl.createTexture();
+ }
+ gl.bindTexture(gl.TEXTURE_2D, texture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+ return texture;
+ }
+ }
+
+ return TextureLoader;
+})();
diff --git a/tests/html/webvr/js/third-party/wglu/wglu-url.js b/tests/html/webvr/js/third-party/wglu/wglu-url.js
new file mode 100644
index 000000000000..43be65831bda
--- /dev/null
+++ b/tests/html/webvr/js/third-party/wglu/wglu-url.js
@@ -0,0 +1,94 @@
+/*
+Copyright (c) 2015, 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.
+*/
+
+/*
+Provides a simple way to get values from the query string if they're present
+and use a default value if not. Not strictly a "WebGL" utility, but I use it
+frequently enough for debugging that I wanted to include it here.
+
+Example:
+For the URL http://example.com/index.html?particleCount=1000
+
+WGLUUrl.getInt("particleCount", 100); // URL overrides, returns 1000
+WGLUUrl.getInt("particleSize", 10); // Not in URL, returns default of 10
+*/
+var WGLUUrl = (function() {
+
+ "use strict";
+
+ var urlArgs = null;
+
+ function ensureArgsCached() {
+ if (!urlArgs) {
+ urlArgs = {};
+ var query = window.location.search.substring(1);
+ var vars = query.split("&");
+ for (var i = 0; i < vars.length; i++) {
+ var pair = vars[i].split("=");
+ urlArgs[pair[0].toLowerCase()] = unescape(pair[1]);
+ }
+ }
+ }
+
+ function getString(name, defaultValue) {
+ ensureArgsCached();
+ var lcaseName = name.toLowerCase();
+ if (lcaseName in urlArgs) {
+ return urlArgs[lcaseName];
+ }
+ return defaultValue;
+ }
+
+ function getInt(name, defaultValue) {
+ ensureArgsCached();
+ var lcaseName = name.toLowerCase();
+ if (lcaseName in urlArgs) {
+ return parseInt(urlArgs[lcaseName], 10);
+ }
+ return defaultValue;
+ }
+
+ function getFloat(name, defaultValue) {
+ ensureArgsCached();
+ var lcaseName = name.toLowerCase();
+ if (lcaseName in urlArgs) {
+ return parseFloat(urlArgs[lcaseName]);
+ }
+ return defaultValue;
+ }
+
+ function getBool(name, defaultValue) {
+ ensureArgsCached();
+ var lcaseName = name.toLowerCase();
+ if (lcaseName in urlArgs) {
+ return parseInt(urlArgs[lcaseName], 10) != 0;
+ }
+ return defaultValue;
+ }
+
+ return {
+ getString: getString,
+ getInt: getInt,
+ getFloat: getFloat,
+ getBool: getBool
+ };
+})();
diff --git a/tests/html/webvr/js/vr-audio-panner.js b/tests/html/webvr/js/vr-audio-panner.js
new file mode 100644
index 000000000000..292d1cc366a8
--- /dev/null
+++ b/tests/html/webvr/js/vr-audio-panner.js
@@ -0,0 +1,284 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+(function (VRAudioPanner) {
+
+ 'use strict';
+
+ // Default settings for panning. Cone parameters are experimentally
+ // determined.
+ var _PANNING_MODEL = 'HRTF';
+ var _DISTANCE_MODEL = 'inverse';
+ var _CONE_INNER_ANGLE = 60;
+ var _CONE_OUTER_ANGLE = 120;
+ var _CONE_OUTER_GAIN = 0.25;
+
+ // Super-simple web audio version detection.
+ var _LEGACY_WEBAUDIO = window.hasOwnProperty('webkitAudioContext') && !window.hasOwnProperty('AudioContext');
+ if (_LEGACY_WEBAUDIO)
+ console.log('[VRAudioPanner] outdated version of Web Audio API detected.');
+
+ // Master audio context.
+ var _context = _LEGACY_WEBAUDIO ? new webkitAudioContext() : new AudioContext();
+
+
+ /**
+ * A buffer source player with HRTF panning for testing purpose.
+ * @param {Object} options Default options.
+ * @param {Number} options.gain Sound object gain. (0.0~1.0)
+ * @param {Number} options.buffer AudioBuffer to play.
+ * @param {Number} options.detune Detune parameter. (cent)
+ * @param {Array} options.position x, y, z position in a array.
+ */
+ function TestSource (options) {
+
+ this._src = _context.createBufferSource();
+ this._out = _context.createGain();
+ this._panner = _context.createPanner();
+ this._analyser = _context.createAnalyser();
+
+ this._src.connect(this._out);
+ this._out.connect(this._analyser);
+ this._analyser.connect(this._panner);
+ this._panner.connect(_context.destination);
+
+ this._src.buffer = options.buffer;
+ this._src.loop = true;
+ this._out.gain.value = options.gain;
+
+ this._analyser.fftSize = 1024;
+ this._analyser.smoothingTimeConstant = 0.85;
+ this._lastRMSdB = 0.0;
+
+ this._panner.panningModel = _PANNING_MODEL;
+ this._panner.distanceModel = _DISTANCE_MODEL;
+ this._panner.coneInnerAngle = _CONE_INNER_ANGLE;
+ this._panner.coneOuterAngle = _CONE_OUTER_ANGLE;
+ this._panner.coneOuterGain = _CONE_OUTER_GAIN;
+
+ this._position = [0, 0, 0];
+ this._orientation = [1, 0, 0];
+
+ this._analyserBuffer = new Uint8Array(this._analyser.fftSize);
+
+ if (!_LEGACY_WEBAUDIO) {
+ this._src.detune.value = (options.detune || 0);
+ this._analyserBuffer = new Float32Array(this._analyser.fftSize);
+ }
+
+ this.setPosition(options.position);
+ this.setOrientation(options.orientation);
+
+ };
+
+ TestSource.prototype.start = function () {
+ this._src.start(0);
+ };
+
+ TestSource.prototype.stop = function () {
+ this._src.stop(0);
+ };
+
+ TestSource.prototype.getPosition = function () {
+ return this._position;
+ };
+
+ TestSource.prototype.setPosition = function (position) {
+ if (position) {
+ this._position[0] = position[0];
+ this._position[1] = position[1];
+ this._position[2] = position[2];
+ }
+
+ this._panner.setPosition.apply(this._panner, this._position);
+ };
+
+ TestSource.prototype.getOrientation = function () {
+ return this._orientation;
+ };
+
+ TestSource.prototype.setOrientation = function (orientation) {
+ if (orientation) {
+ this._orientation[0] = orientation[0];
+ this._orientation[1] = orientation[1];
+ this._orientation[2] = orientation[2];
+ }
+
+ this._panner.setOrientation.apply(this._panner, this._orientation);
+ };
+
+ TestSource.prototype.getCubeScale = function () {
+ // Safari does not support getFloatTimeDomainData(), so fallback to the
+ // naive spectral energy sum. This is relative expensive.
+ if (_LEGACY_WEBAUDIO) {
+ this._analyser.getByteFrequencyData(this._analyserBuffer);
+
+ for (var k = 0, total = 0; k < this._analyserBuffer.length; ++k)
+ total += this._analyserBuffer[k];
+ total /= this._analyserBuffer.length;
+
+ return (total / 256.0) * 1.5;
+ }
+
+ this._analyser.getFloatTimeDomainData(this._analyserBuffer);
+ for (var i = 0, sum = 0; i < this._analyserBuffer.length; ++i)
+ sum += this._analyserBuffer[i] * this._analyserBuffer[i];
+
+ // Calculate RMS and convert it to DB for perceptual loudness.
+ var rms = Math.sqrt(sum / this._analyserBuffer.length);
+ var db = 30 + 10 / Math.LN10 * Math.log(rms <= 0 ? 0.0001 : rms);
+
+ // Moving average with the alpha of 0.525. Experimentally determined.
+ this._lastRMSdB += 0.525 * ((db < 0 ? 0 : db) - this._lastRMSdB);
+
+ // Scaling by 1/30 is also experimentally determined.
+ return this._lastRMSdB / 30.0;
+ };
+
+
+ // Internal helper: load a file into a buffer. (github.com/hoch/spiral)
+ function _loadAudioFile(context, fileInfo, done) {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', fileInfo.url);
+ xhr.responseType = 'arraybuffer';
+
+ xhr.onload = function () {
+ if (xhr.status === 200) {
+ context.decodeAudioData(xhr.response,
+ function (buffer) {
+ console.log('[VRAudioPanner] File loaded: ' + fileInfo.url);
+ done(fileInfo.name, buffer);
+ },
+ function (message) {
+ console.log('[VRAudioPanner] Decoding failure: ' + fileInfo.url + ' (' + message + ')');
+ done(fileInfo.name, null);
+ });
+ } else {
+ console.log('[VRAudioPanner] XHR Error: ' + fileInfo.url + ' (' + xhr.statusText + ')');
+ done(fileInfo.name, null);
+ }
+ };
+
+ xhr.onerror = function (event) {
+ console.log('[VRAudioPanner] XHR Network failure: ' + fileInfo.url);
+ done(fileInfo.name, null);
+ };
+
+ xhr.send();
+ }
+
+
+ /**
+ * A wrapper/container class for multiple file loaders.
+ * @param {Object} context AudioContext
+ * @param {Object} audioFileData Audio file info in the format of {name, url}
+ * @param {Function} resolve Resolution handler for promise.
+ * @param {Function} reject Rejection handler for promise.
+ * @param {Function} progress Progress event handler.
+ */
+ function AudioBufferManager(context, audioFileData, resolve, reject, progress) {
+ this._context = context;
+ this._resolve = resolve;
+ this._reject = reject;
+ this._progress = progress;
+
+ this._buffers = new Map();
+ this._loadingTasks = {};
+
+ // Iterating file loading.
+ for (var i = 0; i < audioFileData.length; i++) {
+ var fileInfo = audioFileData[i];
+
+ // Check for duplicates filename and quit if it happens.
+ if (this._loadingTasks.hasOwnProperty(fileInfo.name)) {
+ console.log('[VRAudioPanner] Duplicated filename in AudioBufferManager: ' + fileInfo.name);
+ return;
+ }
+
+ // Mark it as pending (0)
+ this._loadingTasks[fileInfo.name] = 0;
+ _loadAudioFile(this._context, fileInfo, this._done.bind(this));
+ }
+ }
+
+ AudioBufferManager.prototype._done = function (filename, buffer) {
+ // Label the loading task.
+ this._loadingTasks[filename] = buffer !== null ? 'loaded' : 'failed';
+
+ // A failed task will be a null buffer.
+ this._buffers.set(filename, buffer);
+
+ this._updateProgress(filename);
+ };
+
+ AudioBufferManager.prototype._updateProgress = function (filename) {
+ var numberOfFinishedTasks = 0, numberOfFailedTask = 0;
+ var numberOfTasks = 0;
+
+ for (var task in this._loadingTasks) {
+ numberOfTasks++;
+ if (this._loadingTasks[task] === 'loaded')
+ numberOfFinishedTasks++;
+ else if (this._loadingTasks[task] === 'failed')
+ numberOfFailedTask++;
+ }
+
+ if (typeof this._progress === 'function')
+ this._progress(filename, numberOfFinishedTasks, numberOfTasks);
+
+ if (numberOfFinishedTasks === numberOfTasks)
+ this._resolve(this._buffers);
+
+ if (numberOfFinishedTasks + numberOfFailedTask === numberOfTasks)
+ this._reject(this._buffers);
+ };
+
+ /**
+ * Returns true if the web audio implementation is outdated.
+ * @return {Boolean}
+ */
+ VRAudioPanner.isWebAudioOutdated = function () {
+ return _LEGACY_WEBAUDIO;
+ }
+
+ /**
+ * Static method for updating listener's position.
+ * @param {Array} position Listener position in x, y, z.
+ */
+ VRAudioPanner.setListenerPosition = function (position) {
+ _context.listener.setPosition.apply(_context.listener, position);
+ };
+
+ /**
+ * Static method for updating listener's orientation.
+ * @param {Array} orientation Listener orientation in x, y, z.
+ * @param {Array} orientation Listener's up vector in x, y, z.
+ */
+ VRAudioPanner.setListenerOrientation = function (orientation, upvector) {
+ _context.listener.setOrientation(
+ orientation[0], orientation[1], orientation[2],
+ upvector[0], upvector[1], upvector[2]);
+ };
+
+ /**
+ * Load an audio file asynchronously.
+ * @param {Array} dataModel Audio file info in the format of {name, url}
+ * @param {Function} onprogress Callback function for reporting the progress.
+ * @return {Promise} Promise.
+ */
+ VRAudioPanner.loadAudioFiles = function (dataModel, onprogress) {
+ return new Promise(function (resolve, reject) {
+ new AudioBufferManager(_context, dataModel, resolve, reject, onprogress);
+ });
+ };
+
+ /**
+ * Create a source player. See TestSource class for parameter description.
+ * @return {TestSource}
+ */
+ VRAudioPanner.createTestSource = function (options) {
+ return new TestSource(options);
+ };
+
+})(VRAudioPanner = {});
diff --git a/tests/html/webvr/js/vr-cube-island.js b/tests/html/webvr/js/vr-cube-island.js
new file mode 100644
index 000000000000..e21a10e11bd2
--- /dev/null
+++ b/tests/html/webvr/js/vr-cube-island.js
@@ -0,0 +1,210 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/* global mat4, WGLUProgram */
+
+/*
+Like CubeSea, but designed around a users physical space. One central platform
+that maps to the users play area and several floating cubes that sit just
+those boundries (just to add visual interest)
+*/
+window.VRCubeIsland = (function () {
+ "use strict";
+
+ var cubeIslandVS = [
+ "uniform mat4 projectionMat;",
+ "uniform mat4 modelViewMat;",
+ "attribute vec3 position;",
+ "attribute vec2 texCoord;",
+ "varying vec2 vTexCoord;",
+
+ "void main() {",
+ " vTexCoord = texCoord;",
+ " gl_Position = projectionMat * modelViewMat * vec4( position, 1.0 );",
+ "}",
+ ].join("\n");
+
+ var cubeIslandFS = [
+ "precision mediump float;",
+ "uniform sampler2D diffuse;",
+ "varying vec2 vTexCoord;",
+
+ "void main() {",
+ " gl_FragColor = texture2D(diffuse, vTexCoord);",
+ "}",
+ ].join("\n");
+
+ var CubeIsland = function (gl, texture, width, depth) {
+ this.gl = gl;
+
+ this.statsMat = mat4.create();
+
+ this.texture = texture;
+
+ this.program = new WGLUProgram(gl);
+ this.program.attachShaderSource(cubeIslandVS, gl.VERTEX_SHADER);
+ this.program.attachShaderSource(cubeIslandFS, gl.FRAGMENT_SHADER);
+ this.program.bindAttribLocation({
+ position: 0,
+ texCoord: 1
+ });
+ this.program.link();
+
+ this.vertBuffer = gl.createBuffer();
+ this.indexBuffer = gl.createBuffer();
+
+ this.resize(width, depth);
+ };
+
+ CubeIsland.prototype.resize = function (width, depth) {
+ var gl = this.gl;
+
+ this.width = width;
+ this.depth = depth;
+
+ var cubeVerts = [];
+ var cubeIndices = [];
+
+ // Build a single box.
+ function appendBox (left, bottom, back, right, top, front) {
+ // Bottom
+ var idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 1, idx + 2);
+ cubeIndices.push(idx, idx + 2, idx + 3);
+
+ cubeVerts.push(left, bottom, back, 0.0, 1.0);
+ cubeVerts.push(right, bottom, back, 1.0, 1.0);
+ cubeVerts.push(right, bottom, front, 1.0, 0.0);
+ cubeVerts.push(left, bottom, front, 0.0, 0.0);
+
+ // Top
+ idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 2, idx + 1);
+ cubeIndices.push(idx, idx + 3, idx + 2);
+
+ cubeVerts.push(left, top, back, 0.0, 0.0);
+ cubeVerts.push(right, top, back, 1.0, 0.0);
+ cubeVerts.push(right, top, front, 1.0, 1.0);
+ cubeVerts.push(left, top, front, 0.0, 1.0);
+
+ // Left
+ idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 2, idx + 1);
+ cubeIndices.push(idx, idx + 3, idx + 2);
+
+ cubeVerts.push(left, bottom, back, 0.0, 1.0);
+ cubeVerts.push(left, top, back, 0.0, 0.0);
+ cubeVerts.push(left, top, front, 1.0, 0.0);
+ cubeVerts.push(left, bottom, front, 1.0, 1.0);
+
+ // Right
+ idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 1, idx + 2);
+ cubeIndices.push(idx, idx + 2, idx + 3);
+
+ cubeVerts.push(right, bottom, back, 1.0, 1.0);
+ cubeVerts.push(right, top, back, 1.0, 0.0);
+ cubeVerts.push(right, top, front, 0.0, 0.0);
+ cubeVerts.push(right, bottom, front, 0.0, 1.0);
+
+ // Back
+ idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 2, idx + 1);
+ cubeIndices.push(idx, idx + 3, idx + 2);
+
+ cubeVerts.push(left, bottom, back, 1.0, 1.0);
+ cubeVerts.push(right, bottom, back, 0.0, 1.0);
+ cubeVerts.push(right, top, back, 0.0, 0.0);
+ cubeVerts.push(left, top, back, 1.0, 0.0);
+
+ // Front
+ idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 1, idx + 2);
+ cubeIndices.push(idx, idx + 2, idx + 3);
+
+ cubeVerts.push(left, bottom, front, 0.0, 1.0);
+ cubeVerts.push(right, bottom, front, 1.0, 1.0);
+ cubeVerts.push(right, top, front, 1.0, 0.0);
+ cubeVerts.push(left, top, front, 0.0, 0.0);
+ }
+
+ // Appends a cube with the given centerpoint and size.
+ function appendCube (x, y, z, size) {
+ var halfSize = size * 0.5;
+ appendBox(x - halfSize, y - halfSize, z - halfSize,
+ x + halfSize, y + halfSize, z + halfSize);
+ }
+
+ // Main "island", covers where the user can safely stand. Top of the cube
+ // (the ground the user stands on) should be at Y=0 to align with users
+ // floor. X=0 and Z=0 should be at the center of the users play space.
+ appendBox(-width * 0.5, -width, -depth * 0.5, width * 0.5, 0, depth * 0.5);
+
+ // A sprinkling of other cubes to make things more visually interesting.
+ appendCube(1.1, 0.3, (-depth * 0.5) - 0.8, 0.5);
+ appendCube(-0.5, 1.0, (-depth * 0.5) - 0.9, 0.75);
+ appendCube(0.6, 1.5, (-depth * 0.5) - 0.6, 0.4);
+ appendCube(-1.0, 0.5, (-depth * 0.5) - 0.5, 0.2);
+
+ appendCube((-width * 0.5) - 0.8, 0.3, -1.1, 0.5);
+ appendCube((-width * 0.5) - 0.9, 1.0, 0.5, 0.75);
+ appendCube((-width * 0.5) - 0.6, 1.5, -0.6, 0.4);
+ appendCube((-width * 0.5) - 0.5, 0.5, 1.0, 0.2);
+
+ appendCube((width * 0.5) + 0.8, 0.3, 1.1, 0.5);
+ appendCube((width * 0.5) + 0.9, 1.0, -0.5, 0.75);
+ appendCube((width * 0.5) + 0.6, 1.5, 0.6, 0.4);
+ appendCube((width * 0.5) + 0.5, 0.5, -1.0, 0.2);
+
+ appendCube(1.1, 1.4, (depth * 0.5) + 0.8, 0.5);
+ appendCube(-0.5, 1.0, (depth * 0.5) + 0.9, 0.75);
+ appendCube(0.6, 0.4, (depth * 0.5) + 0.6, 0.4);
+
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(cubeVerts), gl.STATIC_DRAW);
+
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeIndices), gl.STATIC_DRAW);
+
+ this.indexCount = cubeIndices.length;
+ };
+
+ CubeIsland.prototype.render = function (projectionMat, modelViewMat, stats) {
+ var gl = this.gl;
+ var program = this.program;
+
+ program.use();
+
+ gl.uniformMatrix4fv(program.uniform.projectionMat, false, projectionMat);
+ gl.uniformMatrix4fv(program.uniform.modelViewMat, false, modelViewMat);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
+
+ gl.enableVertexAttribArray(program.attrib.position);
+ gl.enableVertexAttribArray(program.attrib.texCoord);
+
+ gl.vertexAttribPointer(program.attrib.position, 3, gl.FLOAT, false, 20, 0);
+ gl.vertexAttribPointer(program.attrib.texCoord, 2, gl.FLOAT, false, 20, 12);
+
+ gl.activeTexture(gl.TEXTURE0);
+ gl.uniform1i(this.program.uniform.diffuse, 0);
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
+
+ gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_SHORT, 0);
+
+ if (stats) {
+ // To ensure that the FPS counter is visible in VR mode we have to
+ // render it as part of the scene.
+ mat4.fromTranslation(this.statsMat, [0, 1.5, -this.depth * 0.5]);
+ mat4.scale(this.statsMat, this.statsMat, [0.5, 0.5, 0.5]);
+ mat4.rotateX(this.statsMat, this.statsMat, -0.75);
+ mat4.multiply(this.statsMat, modelViewMat, this.statsMat);
+ stats.render(projectionMat, this.statsMat);
+ }
+ };
+
+ return CubeIsland;
+})();
diff --git a/tests/html/webvr/js/vr-cube-sea.js b/tests/html/webvr/js/vr-cube-sea.js
new file mode 100644
index 000000000000..5002e1816396
--- /dev/null
+++ b/tests/html/webvr/js/vr-cube-sea.js
@@ -0,0 +1,188 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/* global mat4, WGLUProgram */
+
+window.VRCubeSea = (function () {
+ "use strict";
+
+ var cubeSeaVS = [
+ "uniform mat4 projectionMat;",
+ "uniform mat4 modelViewMat;",
+ "attribute vec3 position;",
+ "attribute vec2 texCoord;",
+ "varying vec2 vTexCoord;",
+
+ "void main() {",
+ " vTexCoord = texCoord;",
+ " gl_Position = projectionMat * modelViewMat * vec4( position, 1.0 );",
+ "}",
+ ].join("\n");
+
+ var cubeSeaFS = [
+ "precision mediump float;",
+ "uniform sampler2D diffuse;",
+ "varying vec2 vTexCoord;",
+
+ "void main() {",
+ " gl_FragColor = texture2D(diffuse, vTexCoord);",
+ "}",
+ ].join("\n");
+
+ var CubeSea = function (gl, texture) {
+ this.gl = gl;
+
+ this.statsMat = mat4.create();
+
+ this.texture = texture;
+
+ this.program = new WGLUProgram(gl);
+ this.program.attachShaderSource(cubeSeaVS, gl.VERTEX_SHADER);
+ this.program.attachShaderSource(cubeSeaFS, gl.FRAGMENT_SHADER);
+ this.program.bindAttribLocation({
+ position: 0,
+ texCoord: 1
+ });
+ this.program.link();
+
+ var cubeVerts = [];
+ var cubeIndices = [];
+
+ // Build a single cube.
+ function appendCube (x, y, z) {
+ if (!x && !y && !z) {
+ // Don't create a cube in the center.
+ return;
+ }
+
+ var size = 0.2;
+ // Bottom
+ var idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 1, idx + 2);
+ cubeIndices.push(idx, idx + 2, idx + 3);
+
+ cubeVerts.push(x - size, y - size, z - size, 0.0, 1.0);
+ cubeVerts.push(x + size, y - size, z - size, 1.0, 1.0);
+ cubeVerts.push(x + size, y - size, z + size, 1.0, 0.0);
+ cubeVerts.push(x - size, y - size, z + size, 0.0, 0.0);
+
+ // Top
+ idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 2, idx + 1);
+ cubeIndices.push(idx, idx + 3, idx + 2);
+
+ cubeVerts.push(x - size, y + size, z - size, 0.0, 0.0);
+ cubeVerts.push(x + size, y + size, z - size, 1.0, 0.0);
+ cubeVerts.push(x + size, y + size, z + size, 1.0, 1.0);
+ cubeVerts.push(x - size, y + size, z + size, 0.0, 1.0);
+
+ // Left
+ idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 2, idx + 1);
+ cubeIndices.push(idx, idx + 3, idx + 2);
+
+ cubeVerts.push(x - size, y - size, z - size, 0.0, 1.0);
+ cubeVerts.push(x - size, y + size, z - size, 0.0, 0.0);
+ cubeVerts.push(x - size, y + size, z + size, 1.0, 0.0);
+ cubeVerts.push(x - size, y - size, z + size, 1.0, 1.0);
+
+ // Right
+ idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 1, idx + 2);
+ cubeIndices.push(idx, idx + 2, idx + 3);
+
+ cubeVerts.push(x + size, y - size, z - size, 1.0, 1.0);
+ cubeVerts.push(x + size, y + size, z - size, 1.0, 0.0);
+ cubeVerts.push(x + size, y + size, z + size, 0.0, 0.0);
+ cubeVerts.push(x + size, y - size, z + size, 0.0, 1.0);
+
+ // Back
+ idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 2, idx + 1);
+ cubeIndices.push(idx, idx + 3, idx + 2);
+
+ cubeVerts.push(x - size, y - size, z - size, 1.0, 1.0);
+ cubeVerts.push(x + size, y - size, z - size, 0.0, 1.0);
+ cubeVerts.push(x + size, y + size, z - size, 0.0, 0.0);
+ cubeVerts.push(x - size, y + size, z - size, 1.0, 0.0);
+
+ // Front
+ idx = cubeVerts.length / 5.0;
+ cubeIndices.push(idx, idx + 1, idx + 2);
+ cubeIndices.push(idx, idx + 2, idx + 3);
+
+ cubeVerts.push(x - size, y - size, z + size, 0.0, 1.0);
+ cubeVerts.push(x + size, y - size, z + size, 1.0, 1.0);
+ cubeVerts.push(x + size, y + size, z + size, 1.0, 0.0);
+ cubeVerts.push(x - size, y + size, z + size, 0.0, 0.0);
+ }
+
+ var gridSize = 10;
+
+ // Build the cube sea
+ for (var x = 0; x < gridSize; ++x) {
+ for (var y = 0; y < gridSize; ++y) {
+ for (var z = 0; z < gridSize; ++z) {
+ appendCube(x - (gridSize / 2), y - (gridSize / 2), z - (gridSize / 2));
+ }
+ }
+ }
+
+ this.vertBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(cubeVerts), gl.STATIC_DRAW);
+
+ this.indexBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeIndices), gl.STATIC_DRAW);
+
+ this.indexCount = cubeIndices.length;
+ };
+
+ var mortimer = mat4.create();
+
+ var a = [0.9868122935295105, -0.03754837438464165, -0.15745431184768677, 0, 0.011360996402800083, 0.9863911271095276, -0.1640235036611557, 0, 0.16147033870220184, 0.16007155179977417, 0.9738093614578247, 0, 0.192538782954216, 0.024526841938495636, -0.001076754298992455, 1.0000001192092896];
+ for (var i = 0; i < 16; ++i) {
+ mortimer[i] = a[i];
+ }
+
+ CubeSea.prototype.render = function (projectionMat, modelViewMat, stats) {
+ var gl = this.gl;
+ var program = this.program;
+
+ //mat4.invert(mortimer, modelViewMat);
+
+ program.use();
+
+ gl.uniformMatrix4fv(program.uniform.projectionMat, false, projectionMat);
+ gl.uniformMatrix4fv(program.uniform.modelViewMat, false, modelViewMat);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
+
+ gl.enableVertexAttribArray(program.attrib.position);
+ gl.enableVertexAttribArray(program.attrib.texCoord);
+
+ gl.vertexAttribPointer(program.attrib.position, 3, gl.FLOAT, false, 20, 0);
+ gl.vertexAttribPointer(program.attrib.texCoord, 2, gl.FLOAT, false, 20, 12);
+
+ gl.activeTexture(gl.TEXTURE0);
+ gl.uniform1i(this.program.uniform.diffuse, 0);
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
+
+ gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_SHORT, 0);
+
+ if (stats) {
+ // To ensure that the FPS counter is visible in VR mode we have to
+ // render it as part of the scene.
+ mat4.fromTranslation(this.statsMat, [0, -0.3, -0.5]);
+ mat4.scale(this.statsMat, this.statsMat, [0.3, 0.3, 0.3]);
+ mat4.rotateX(this.statsMat, this.statsMat, -0.75);
+ mat4.multiply(this.statsMat, modelViewMat, this.statsMat);
+ stats.render(projectionMat, this.statsMat);
+ }
+ };
+
+ return CubeSea;
+})();
diff --git a/tests/html/webvr/js/vr-panorama.js b/tests/html/webvr/js/vr-panorama.js
new file mode 100644
index 000000000000..8eac81e9f05e
--- /dev/null
+++ b/tests/html/webvr/js/vr-panorama.js
@@ -0,0 +1,219 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/* global mat4, WGLUProgram */
+
+window.VRPanorama = (function () {
+ "use strict";
+
+ var panoVS = [
+ "uniform mat4 projectionMat;",
+ "uniform mat4 modelViewMat;",
+ "attribute vec3 position;",
+ "attribute vec2 texCoord;",
+ "varying vec2 vTexCoord;",
+
+ "void main() {",
+ " vTexCoord = texCoord;",
+ " gl_Position = projectionMat * modelViewMat * vec4( position, 1.0 );",
+ "}",
+ ].join("\n");
+
+ var panoFS = [
+ "precision mediump float;",
+ "uniform sampler2D diffuse;",
+ "varying vec2 vTexCoord;",
+
+ "void main() {",
+ " gl_FragColor = texture2D(diffuse, vTexCoord);",
+ "}",
+ ].join("\n");
+
+ var Panorama = function (gl) {
+ this.gl = gl;
+
+ this.texture = gl.createTexture();
+
+ this.program = new WGLUProgram(gl);
+ this.program.attachShaderSource(panoVS, gl.VERTEX_SHADER);
+ this.program.attachShaderSource(panoFS, gl.FRAGMENT_SHADER);
+ this.program.bindAttribLocation({
+ position: 0,
+ texCoord: 1
+ });
+ this.program.link();
+
+ var panoVerts = [];
+ var panoIndices = [];
+
+ var radius = 2; // 2 meter radius sphere
+ var latSegments = 40;
+ var lonSegments = 40;
+
+ // Create the vertices
+ for (var i=0; i <= latSegments; ++i) {
+ var theta = i * Math.PI / latSegments;
+ var sinTheta = Math.sin(theta);
+ var cosTheta = Math.cos(theta);
+
+ for (var j=0; j <= lonSegments; ++j) {
+ var phi = j * 2 * Math.PI / lonSegments;
+ var sinPhi = Math.sin(phi);
+ var cosPhi = Math.cos(phi);
+
+ var x = sinPhi * sinTheta;
+ var y = cosTheta;
+ var z = -cosPhi * sinTheta;
+ var u = (j / lonSegments);
+ var v = (i / latSegments);
+
+ panoVerts.push(x * radius, y * radius, z * radius, u, v);
+ }
+ }
+
+ // Create the indices
+ for (var i = 0; i < latSegments; ++i) {
+ var offset0 = i * (lonSegments+1);
+ var offset1 = (i+1) * (lonSegments+1);
+ for (var j = 0; j < lonSegments; ++j) {
+ var index0 = offset0+j;
+ var index1 = offset1+j;
+ panoIndices.push(
+ index0, index1, index0+1,
+ index1, index1+1, index0+1
+ );
+ }
+ }
+
+ this.vertBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(panoVerts), gl.STATIC_DRAW);
+
+ this.indexBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(panoIndices), gl.STATIC_DRAW);
+
+ this.indexCount = panoIndices.length;
+
+ this.imgElement = null;
+ this.videoElement = null;
+ };
+
+ Panorama.prototype.setImage = function (url) {
+ var gl = this.gl;
+ var self = this;
+
+ return new Promise(function(resolve, reject) {
+ var img = new Image();
+ img.addEventListener('load', function() {
+ self.imgElement = img;
+ self.videoElement = null;
+
+ gl.bindTexture(gl.TEXTURE_2D, self.texture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
+
+ 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);
+
+ resolve(self.imgElement);
+ });
+ img.addEventListener('error', function(ev) {
+ console.error(ev.message);
+ reject(ev.message);
+ }, false);
+ img.crossOrigin = 'anonymous';
+ img.src = url;
+ });
+ };
+
+ Panorama.prototype.setVideo = function (url) {
+ var gl = this.gl;
+ var self = this;
+
+ return new Promise(function(resolve, reject) {
+ var video = document.createElement('video');
+ video.addEventListener('canplay', function() {
+ // Added "click to play" UI?
+ });
+
+ video.addEventListener('playing', function() {
+ self.videoElement = video;
+ self.imgElement = null;
+
+ gl.bindTexture(gl.TEXTURE_2D, self.texture);
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, self.videoElement);
+
+ 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);
+
+ resolve(self.videoElement);
+ });
+
+ video.addEventListener('error', function(ev) {
+ console.error(video.error);
+ reject(video.error);
+ }, false);
+
+ video.loop = true;
+ video.autoplay = true;
+ video.crossOrigin = 'anonymous';
+ video.setAttribute('webkit-playsinline', '');
+ video.src = url;
+ });
+ };
+
+ Panorama.prototype.play = function() {
+ if (this.videoElement)
+ this.videoElement.play();
+ };
+
+ Panorama.prototype.pause = function() {
+ if (this.videoElement)
+ this.videoElement.pause();
+ };
+
+ Panorama.prototype.isPaused = function() {
+ if (this.videoElement)
+ return this.videoElement.paused;
+ return false;
+ };
+
+ Panorama.prototype.render = function (projectionMat, modelViewMat) {
+ var gl = this.gl;
+ var program = this.program;
+
+ if (!this.imgElement && !this.videoElement)
+ return;
+
+ program.use();
+
+ gl.uniformMatrix4fv(program.uniform.projectionMat, false, projectionMat);
+ gl.uniformMatrix4fv(program.uniform.modelViewMat, false, modelViewMat);
+
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vertBuffer);
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
+
+ gl.enableVertexAttribArray(program.attrib.position);
+ gl.enableVertexAttribArray(program.attrib.texCoord);
+
+ gl.vertexAttribPointer(program.attrib.position, 3, gl.FLOAT, false, 20, 0);
+ gl.vertexAttribPointer(program.attrib.texCoord, 2, gl.FLOAT, false, 20, 12);
+
+ gl.activeTexture(gl.TEXTURE0);
+ gl.uniform1i(this.program.uniform.diffuse, 0);
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
+
+ if (this.videoElement && !this.videoElement.paused) {
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.videoElement);
+ }
+
+ gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_SHORT, 0);
+ };
+
+ return Panorama;
+})();
diff --git a/tests/html/webvr/js/vr-samples-util.js b/tests/html/webvr/js/vr-samples-util.js
new file mode 100644
index 000000000000..20553bdf8702
--- /dev/null
+++ b/tests/html/webvr/js/vr-samples-util.js
@@ -0,0 +1,181 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+window.VRSamplesUtil = (function () {
+
+ "use strict";
+
+ // Lifted from the WebVR Polyfill
+ function isMobile () {
+ return /Android/i.test(navigator.userAgent) ||
+ /iPhone|iPad|iPod/i.test(navigator.userAgent);
+ }
+
+ function getMessageContainer () {
+ var messageContainer = document.getElementById("vr-sample-message-container");
+ if (!messageContainer) {
+ messageContainer = document.createElement("div");
+ messageContainer.id = "vr-sample-message-container";
+ messageContainer.style.fontFamily = "sans-serif";
+ messageContainer.style.position = "absolute";
+ messageContainer.style.zIndex = "999";
+ messageContainer.style.left = "0";
+ messageContainer.style.top = "0";
+ messageContainer.style.right = "0";
+ messageContainer.style.margin = "0";
+ messageContainer.style.padding = "0";
+ messageContainer.align = "center";
+ document.body.appendChild(messageContainer);
+ }
+ return messageContainer;
+ }
+
+ function addMessageElement (message, backgroundColor) {
+ var messageElement = document.createElement("div");
+ messageElement.classList.add = "vr-sample-message";
+ messageElement.style.color = "#FFF";
+ messageElement.style.backgroundColor = backgroundColor;
+ messageElement.style.borderRadius = "3px";
+ messageElement.style.position = "relative";
+ messageElement.style.display = "inline-block";
+ messageElement.style.margin = "0.5em";
+ messageElement.style.padding = "0.75em";
+
+ messageElement.innerHTML = message;
+
+ getMessageContainer().appendChild(messageElement);
+
+ return messageElement;
+ }
+
+ // Makes the given element fade out and remove itself from the DOM after the
+ // given timeout.
+ function makeToast (element, timeout) {
+ element.style.transition = "opacity 0.5s ease-in-out";
+ element.style.opacity = "1";
+ setTimeout(function () {
+ element.style.opacity = "0";
+ setTimeout(function () {
+ if (element.parentElement)
+ element.parentElement.removeChild(element);
+ }, 500);
+ }, timeout);
+ }
+
+ function addError (message, timeout) {
+ var element = addMessageElement("ERROR: " + message, "#D33");
+
+ if (timeout) {
+ makeToast(element, timeout);
+ }
+
+ return element;
+ }
+
+ function addInfo (message, timeout) {
+ var element = addMessageElement(message, "#22A");
+
+ if (timeout) {
+ makeToast(element, timeout);
+ }
+
+ return element;
+ }
+
+ function getButtonContainer () {
+ var buttonContainer = document.getElementById("vr-sample-button-container");
+ if (!buttonContainer) {
+ buttonContainer = document.createElement("div");
+ buttonContainer.id = "vr-sample-button-container";
+ buttonContainer.style.fontFamily = "sans-serif";
+ buttonContainer.style.position = "absolute";
+ buttonContainer.style.zIndex = "999";
+ buttonContainer.style.left = "0";
+ buttonContainer.style.bottom = "0";
+ buttonContainer.style.right = "0";
+ buttonContainer.style.margin = "0";
+ buttonContainer.style.padding = "0";
+ buttonContainer.align = "right";
+ document.body.appendChild(buttonContainer);
+ }
+ return buttonContainer;
+ }
+
+ function addButtonElement (message, key, icon) {
+ var buttonElement = document.createElement("div");
+ buttonElement.classList.add = "vr-sample-button";
+ buttonElement.style.color = "#FFF";
+ buttonElement.style.fontWeight = "bold";
+ buttonElement.style.backgroundColor = "#888";
+ buttonElement.style.borderRadius = "5px";
+ buttonElement.style.border = "3px solid #555";
+ buttonElement.style.position = "relative";
+ buttonElement.style.display = "inline-block";
+ buttonElement.style.margin = "0.5em";
+ buttonElement.style.padding = "0.75em";
+ buttonElement.style.cursor = "pointer";
+ buttonElement.align = "center";
+
+ if (icon) {
+ buttonElement.innerHTML = "
" + message;
+ } else {
+ buttonElement.innerHTML = message;
+ }
+
+ if (key) {
+ var keyElement = document.createElement("span");
+ keyElement.classList.add = "vr-sample-button-accelerator";
+ keyElement.style.fontSize = "0.75em";
+ keyElement.style.fontStyle = "italic";
+ keyElement.innerHTML = " (" + key + ")";
+
+ buttonElement.appendChild(keyElement);
+ }
+
+ getButtonContainer().appendChild(buttonElement);
+
+ return buttonElement;
+ }
+
+ function addButton (message, key, icon, callback) {
+ var keyListener = null;
+ if (key) {
+ var keyCode = key.charCodeAt(0);
+ keyListener = function (event) {
+ if (event.keyCode === keyCode) {
+ callback(event);
+ }
+ };
+ document.addEventListener("keydown", keyListener, false);
+ }
+ var element = addButtonElement(message, key, icon);
+ element.addEventListener("click", function (event) {
+ callback(event);
+ event.preventDefault();
+ }, false);
+
+ return {
+ element: element,
+ keyListener: keyListener
+ };
+ }
+
+ function removeButton (button) {
+ if (!button)
+ return;
+ if (button.element.parentElement)
+ button.element.parentElement.removeChild(button.element);
+ if (button.keyListener)
+ document.removeEventListener("keydown", button.keyListener, false);
+ }
+
+ return {
+ isMobile: isMobile,
+ addError: addError,
+ addInfo: addInfo,
+ addButton: addButton,
+ removeButton: removeButton,
+ makeToast: makeToast
+ };
+})();
diff --git a/tests/html/webvr/media/icons/cardboard64.png b/tests/html/webvr/media/icons/cardboard64.png
new file mode 100644
index 000000000000..9457f7d53ed0
Binary files /dev/null and b/tests/html/webvr/media/icons/cardboard64.png differ
diff --git a/tests/html/webvr/media/textures/cube-sea.png b/tests/html/webvr/media/textures/cube-sea.png
new file mode 100644
index 000000000000..356bc3369d31
Binary files /dev/null and b/tests/html/webvr/media/textures/cube-sea.png differ
diff --git a/tests/html/webvr/room-scale.html b/tests/html/webvr/room-scale.html
new file mode 100644
index 000000000000..c39d4f56908c
--- /dev/null
+++ b/tests/html/webvr/room-scale.html
@@ -0,0 +1,312 @@
+
+
+
+
+
+
+
+
+
+ 05 - Room Scale
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/html/webvr/simple-mirroring.html b/tests/html/webvr/simple-mirroring.html
new file mode 100644
index 000000000000..e362e5184ff6
--- /dev/null
+++ b/tests/html/webvr/simple-mirroring.html
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+
+
+
+ 04 - Simple Mirroring
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/html/webvr/vr-presentation.html b/tests/html/webvr/vr-presentation.html
new file mode 100644
index 000000000000..3ecb848fab4a
--- /dev/null
+++ b/tests/html/webvr/vr-presentation.html
@@ -0,0 +1,307 @@
+
+
+
+
+
+
+
+
+
+ 03 - VR Presentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Put on your headset now
+
+
+