block by gabrielflorit 89e567f3893f365774a8a911fa81ee90

WebXR - first test

Full Screen

flow

Made with blockup

script.js

require('./demo.js')

index.html

<!DOCTYPE html>
<title>blockup</title>
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">

<link href='dist.css' rel='stylesheet' />

<!-- Origin Trial Token, feature = WebXR Device API, origin = https://ngrok.io, expires = 2018-07-05 -->
<meta http-equiv="origin-trial" data-feature="WebXR Device API" data-expires="2018-07-05" content="An7UhyulDMd1aXHYJY/4aBUXSPnff/LpJhr8H3FW/EKLJb7GyuwH8DWrg+W6AYIxNX6f7vxJySrMcX6OkfgQuAYAAABNeyJvcmlnaW4iOiJodHRwczovL25ncm9rLmlvOjQ0MyIsImZlYXR1cmUiOiJXZWJYUkRldmljZSIsImV4cGlyeSI6MTUzMDgzNDIzNH0=">

<!-- <script src='https://cdn.jsdelivr.net/npm/webxr-polyfill@latest/build/webxr-polyfill.js'></script>
<script>var polyfill = new WebXRPolyfill()</script>
 -->

<body>
  <h1>hello</h1>
  <ul class='logs' />
  <button id="xr-button" disabled>XR not found</button>
  <script src='dist.js'></script>
</body>

demo.js

import Log from './log.js'
const log = Log()

// XR globals.
let xrButton = document.getElementById('xr-button')
let xrDevice = null
let xrSession = null
let xrFrameOfRef = null

// WebGL scene globals.
let gl = null

// Checks to see if WebXR is available and, if so, requests an XRDevice
// that is connected to the system and tests it to ensure it supports the
// desired session options.
function initXR () {
  // Is WebXR available on this UA?
  if (navigator.xr) {
    // Request an XRDevice connected to the system.
    navigator.xr.requestDevice().then(device => {
      xrDevice = device
      // If the device allows creation of exclusive sessions,
      // set it as the target of the 'Enter XR' button.
      device.supportsSession({ exclusive: true }).then(() => {
        // Updates the button to start an XR session when clicked.
        xrButton.addEventListener('click', onButtonClicked)
        xrButton.innerHTML = 'Enter XR'
        xrButton.disabled = false
      })
    })
  } else {
    log.message('This browser does not support the WebXR API.')
  }
}

// Called when the user clicks the button to enter XR. If we don't have a
// session already we'll request one, and if we do we'll end it.
function onButtonClicked () {
  if (!xrSession) {
    xrDevice.requestSession({ exclusive: true }).then(onSessionStarted)
  } else {
    xrSession.end()
  }
}

// Called when we've successfully acquired a XRSession. In response we
// will set up the necessary session state and kick off the frame loop.
function onSessionStarted (session) {
  // Save session to global.
  xrSession = session
  xrButton.innerHTML = 'Exit XR'

  // Listen for the sessions 'end' event so we can respond if the user
  // or UA ends the session for any reason.
  session.addEventListener('end', onSessionEnded)

  // Create a WebGL context to render with, initialized to be compatible
  // with the XRDisplay we're presenting to.
  let canvas = document.createElement('canvas')
  gl = canvas.getContext('webgl', {
    compatibleXRDevice: session.device
  })

  // Use the new WebGL context to create a XRWebGLLayer and set it as the
  // sessions baseLayer. This allows any content rendered to the layer to
  // be displayed on the XRDevice.
  session.baseLayer = new window.XRWebGLLayer(session, gl)

  // Get a frame of reference, which is required for querying poses. In
  // this case an 'eyeLevel' frame of reference means that all poses will
  // be relative to the location where the XRDevice was first detected.
  session.requestFrameOfReference('eyeLevel').then(frameOfRef => {
    xrFrameOfRef = frameOfRef
    // Inform the session that we're ready to begin drawing.
    session.requestAnimationFrame(onXRFrame)
  })
}

// // // Called when the user clicks the 'Exit XR' button. In response we end
// // // the session.
// // function onEndSession (session) {
// //   session.end()
// // }

// Called either when the user has explicitly ended the session (like in
// onEndSession()) or when the UA has ended the session for any reason.
// At this point the session object is no longer usable and should be discarded.
function onSessionEnded (event) {
  xrSession = null
  xrButton.innerHTML = 'Enter VR'
  // In this simple case discard the WebGL context too, since we're not
  // rendering anything else to the screen with it.
  gl = null
}

// Called every time the XRSession requests that a new frame be drawn.
function onXRFrame (t, frame) {
  let session = frame.session

  // Inform the session that we're ready for the next frame.
  session.requestAnimationFrame(onXRFrame)

  // Get the XRDevice pose relative to the Frame of Reference we created
  // earlier.
  let pose = frame.getDevicePose(xrFrameOfRef)

  // Getting the pose may fail if, for example, tracking is lost. So we
  // have to check to make sure that we got a valid pose before attempting
  // to render with it. If not in this case we'll just leave the
  // framebuffer cleared, so tracking loss means the scene will simply
  // dissapear.
  if (pose) {
    // If we do have a valid pose, bind the WebGL layer's framebuffer,
    // which is where any content to be displayed on the XRDevice must be
    // rendered.
    gl.bindFramebuffer(gl.FRAMEBUFFER, session.baseLayer.framebuffer)

    // Update the clear color so that we can observe the color in the
    // headset changing over time.
    let time = Date.now()
    gl.clearColor(
      Math.cos(time / 2000),
      Math.cos(time / 4000),
      Math.cos(time / 6000),
      1.0
    )

    // Clear the framebuffer
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

    // Normally you'd loop through each of the views reported by the frame
    // and draw them into the corresponding viewport here, but we're
    // keeping this sample slim so we're not bothering to draw any
    // geometry.
    /* for (let view of frame.views) {
            let viewport = session.baseLayer.getViewport(view);
            gl.viewport(viewport.x, viewport.y,
                        viewport.width, viewport.height);
            // Draw something.
          } */
  }
}

// Start the XR application.
initXR()

demo.js.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "//www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>~/Documents/other/blocks/xr/first/demo.js.html</title>
<meta name="Generator" content="Vim/8.0">
<meta name="plugin-version" content="vim7.4_v2">
<meta name="syntax" content="javascript">
<meta name="settings" content="use_css,pre_wrap,no_foldcolumn,expand_tabs,prevent_copy=">
<meta name="colorscheme" content="OceanicNext">
<style type="text/css">
<!--
pre { white-space: pre-wrap; font-family: monospace; color: #c6c6c6; background-color: #262626; }
body { font-family: monospace; color: #c6c6c6; background-color: #262626; }
* { font-size: 1em; }
.String { color: #87d787; }
.Boolean { color: #ff875f; }
.Function { color: #5f87d7; }
.Structure { color: #d787d7; }
.javaScriptNumber { color: #ff875f; }
.Normal { color: #c6c6c6; background-color: #262626; padding-bottom: 1px; }
.Comment { color: #767676; }
.Special { color: #5fafaf; }
.Identifier { color: #ff5f5f; }
.Statement { color: #ff5f5f; font-weight: bold; }
.Conditional { color: #d787d7; }
.Label { color: #ffd75f; }
.Keyword { color: #d787d7; }
.Include { color: #5f87d7; }
.javaScriptBraces { color: #c6c6c6; }
-->
</style>

<script type='text/javascript'>
<!--

-->
</script>
</head>
<body>
<pre id='vimCodeElement'>
<span class="Include">import</span> Log <span class="Include">from</span> <span class="String">'./log.js'</span>
<span class="Identifier">const</span> log <span class="Normal">=</span> Log<span class="Normal">()</span>

<span class="Comment">// XR globals.</span>
<span class="Identifier">let</span> xrButton <span class="Normal">=</span> document.<span class="Keyword">getElementById</span><span class="Normal">(</span><span class="String">'xr-button'</span><span class="Normal">)</span>
<span class="Identifier">let</span> xrDevice <span class="Normal">=</span> <span class="Boolean">null</span>
<span class="Identifier">let</span> xrSession <span class="Normal">=</span> <span class="Boolean">null</span>
<span class="Identifier">let</span> xrFrameOfRef <span class="Normal">=</span> <span class="Boolean">null</span>

<span class="Comment">// WebGL scene globals.</span>
<span class="Identifier">let</span> gl <span class="Normal">=</span> <span class="Boolean">null</span>

<span class="Comment">// Checks to see if WebXR is available and, if so, requests an XRDevice</span>
<span class="Comment">// that is connected to the system and tests it to ensure it supports the</span>
<span class="Comment">// desired session options.</span>
<span class="Keyword">function</span> <span class="Function">initXR</span> <span class="Normal">()</span> <span class="javaScriptBraces">{</span>
  <span class="Comment">// Is WebXR available on this UA?</span>
  <span class="Conditional">if</span> (<span class="Structure">navigator</span>.xr) <span class="javaScriptBraces">{</span>
    <span class="Comment">// Request an XRDevice connected to the system.</span>
    <span class="Structure">navigator</span>.xr.requestDevice<span class="Normal">()</span>.<span class="Keyword">then</span><span class="Normal">(</span><span class="Special">device</span> <span class="Statement">=&gt;</span> <span class="javaScriptBraces">{</span>
      xrDevice <span class="Normal">=</span> device
      <span class="Comment">// If the device allows creation of exclusive sessions,</span>
      <span class="Comment">// set it as the target of the 'Enter XR' button.</span>
      device.supportsSession<span class="Normal">(</span><span class="javaScriptBraces">{</span> <span class="Label">exclusive</span>: <span class="Boolean">true</span> <span class="javaScriptBraces">}</span><span class="Normal">)</span>.<span class="Keyword">then</span><span class="Normal">(()</span> <span class="Statement">=&gt;</span> <span class="javaScriptBraces">{</span>
        <span class="Comment">// Updates the button to start an XR session when clicked.</span>
        xrButton.<span class="Keyword">addEventListener</span><span class="Normal">(</span><span class="String">'click'</span><span class="Normal">,</span> onButtonClicked<span class="Normal">)</span>
        xrButton.innerHTML <span class="Normal">=</span> <span class="String">'Enter XR'</span>
        xrButton.disabled <span class="Normal">=</span> <span class="Boolean">false</span>
      <span class="javaScriptBraces">}</span><span class="Normal">)</span>
    <span class="javaScriptBraces">}</span><span class="Normal">)</span>
  <span class="javaScriptBraces">}</span> <span class="Conditional">else</span> <span class="javaScriptBraces">{</span>
    log.message<span class="Normal">(</span><span class="String">'This browser does not support the WebXR API.'</span><span class="Normal">)</span>
  <span class="javaScriptBraces">}</span>
<span class="javaScriptBraces">}</span>

<span class="Comment">// Called when the user clicks the button to enter XR. If we don't have a</span>
<span class="Comment">// session already we'll request one, and if we do we'll end it.</span>
<span class="Keyword">function</span> <span class="Function">onButtonClicked</span> <span class="Normal">()</span> <span class="javaScriptBraces">{</span>
  <span class="Conditional">if</span> (!xrSession) <span class="javaScriptBraces">{</span>
    xrDevice.requestSession<span class="Normal">(</span><span class="javaScriptBraces">{</span> <span class="Label">exclusive</span>: <span class="Boolean">true</span> <span class="javaScriptBraces">}</span><span class="Normal">)</span>.<span class="Keyword">then</span><span class="Normal">(</span>onSessionStarted<span class="Normal">)</span>
  <span class="javaScriptBraces">}</span> <span class="Conditional">else</span> <span class="javaScriptBraces">{</span>
    xrSession.end<span class="Normal">()</span>
  <span class="javaScriptBraces">}</span>
<span class="javaScriptBraces">}</span>

<span class="Comment">// Called when we've successfully acquired a XRSession. In response we</span>
<span class="Comment">// will set up the necessary session state and kick off the frame loop.</span>
<span class="Keyword">function</span> <span class="Function">onSessionStarted</span> <span class="Normal">(</span><span class="Special">session</span><span class="Normal">)</span> <span class="javaScriptBraces">{</span>
  <span class="Comment">// Save session to global.</span>
  xrSession <span class="Normal">=</span> session
  xrButton.innerHTML <span class="Normal">=</span> <span class="String">'Exit XR'</span>

  <span class="Comment">// Listen for the sessions 'end' event so we can respond if the user</span>
  <span class="Comment">// or UA ends the session for any reason.</span>
  session.<span class="Keyword">addEventListener</span><span class="Normal">(</span><span class="String">'end'</span><span class="Normal">,</span> onSessionEnded<span class="Normal">)</span>

  <span class="Comment">// Create a WebGL context to render with, initialized to be compatible</span>
  <span class="Comment">// with the XRDisplay we're presenting to.</span>
  <span class="Identifier">let</span> canvas <span class="Normal">=</span> document.<span class="Keyword">createElement</span><span class="Normal">(</span><span class="String">'canvas'</span><span class="Normal">)</span>
  gl <span class="Normal">=</span> canvas.getContext<span class="Normal">(</span><span class="String">'webgl'</span><span class="Normal">,</span> <span class="javaScriptBraces">{</span>
    <span class="Label">compatibleXRDevice</span>: session.device
  <span class="javaScriptBraces">}</span><span class="Normal">)</span>

  <span class="Comment">// Use the new WebGL context to create a XRWebGLLayer and set it as the</span>
  <span class="Comment">// sessions baseLayer. This allows any content rendered to the layer to</span>
  <span class="Comment">// be displayed on the XRDevice.</span>
  session.baseLayer <span class="Normal">=</span> <span class="Identifier">new</span> window.XRWebGLLayer<span class="Normal">(</span>session<span class="Normal">,</span> gl<span class="Normal">)</span>

  <span class="Comment">// Get a frame of reference, which is required for querying poses. In</span>
  <span class="Comment">// this case an 'eyeLevel' frame of reference means that all poses will</span>
  <span class="Comment">// be relative to the location where the XRDevice was first detected.</span>
  session.requestFrameOfReference<span class="Normal">(</span><span class="String">'eyeLevel'</span><span class="Normal">)</span>.<span class="Keyword">then</span><span class="Normal">(</span><span class="Special">frameOfRef</span> <span class="Statement">=&gt;</span> <span class="javaScriptBraces">{</span>
    xrFrameOfRef <span class="Normal">=</span> frameOfRef
    <span class="Comment">// Inform the session that we're ready to begin drawing.</span>
    session.requestAnimationFrame<span class="Normal">(</span>onXRFrame<span class="Normal">)</span>
  <span class="javaScriptBraces">}</span><span class="Normal">)</span>
<span class="javaScriptBraces">}</span>

<span class="Comment">// // // Called when the user clicks the 'Exit XR' button. In response we end</span>
<span class="Comment">// // // the session.</span>
<span class="Comment">// // function onEndSession (session) {</span>
<span class="Comment">// //   session.end()</span>
<span class="Comment">// // }</span>

<span class="Comment">// Called either when the user has explicitly ended the session (like in</span>
<span class="Comment">// onEndSession()) or when the UA has ended the session for any reason.</span>
<span class="Comment">// At this point the session object is no longer usable and should be discarded.</span>
<span class="Keyword">function</span> <span class="Function">onSessionEnded</span> <span class="Normal">(</span><span class="Special">event</span><span class="Normal">)</span> <span class="javaScriptBraces">{</span>
  xrSession <span class="Normal">=</span> <span class="Boolean">null</span>
  xrButton.innerHTML <span class="Normal">=</span> <span class="String">'Enter VR'</span>
  <span class="Comment">// In this simple case discard the WebGL context too, since we're not</span>
  <span class="Comment">// rendering anything else to the screen with it.</span>
  gl <span class="Normal">=</span> <span class="Boolean">null</span>
<span class="javaScriptBraces">}</span>

<span class="Comment">// Called every time the XRSession requests that a new frame be drawn.</span>
<span class="Keyword">function</span> <span class="Function">onXRFrame</span> <span class="Normal">(</span><span class="Special">t</span><span class="Normal">,</span><span class="Special"> frame</span><span class="Normal">)</span> <span class="javaScriptBraces">{</span>
  <span class="Identifier">let</span> session <span class="Normal">=</span> frame.session

  <span class="Comment">// Inform the session that we're ready for the next frame.</span>
  session.requestAnimationFrame<span class="Normal">(</span>onXRFrame<span class="Normal">)</span>

  <span class="Comment">// Get the XRDevice pose relative to the Frame of Reference we created</span>
  <span class="Comment">// earlier.</span>
  <span class="Identifier">let</span> pose <span class="Normal">=</span> frame.getDevicePose<span class="Normal">(</span>xrFrameOfRef<span class="Normal">)</span>

  <span class="Comment">// Getting the pose may fail if, for example, tracking is lost. So we</span>
  <span class="Comment">// have to check to make sure that we got a valid pose before attempting</span>
  <span class="Comment">// to render with it. If not in this case we'll just leave the</span>
  <span class="Comment">// framebuffer cleared, so tracking loss means the scene will simply</span>
  <span class="Comment">// dissapear.</span>
  <span class="Conditional">if</span> (pose) <span class="javaScriptBraces">{</span>
    <span class="Comment">// If we do have a valid pose, bind the WebGL layer's framebuffer,</span>
    <span class="Comment">// which is where any content to be displayed on the XRDevice must be</span>
    <span class="Comment">// rendered.</span>
    gl.bindFramebuffer<span class="Normal">(</span>gl.FRAMEBUFFER<span class="Normal">,</span> session.baseLayer.framebuffer<span class="Normal">)</span>

    <span class="Comment">// Update the clear color so that we can observe the color in the</span>
    <span class="Comment">// headset changing over time.</span>
    <span class="Identifier">let</span> time <span class="Normal">=</span> <span class="Structure">Date</span>.<span class="Keyword">now</span><span class="Normal">()</span>
    gl.clearColor<span class="Normal">(</span>
      <span class="Structure">Math</span>.<span class="Keyword">cos</span><span class="Normal">(</span>time <span class="Normal">/</span> <span class="javaScriptNumber">2000</span><span class="Normal">)</span><span class="Normal">,</span>
      <span class="Structure">Math</span>.<span class="Keyword">cos</span><span class="Normal">(</span>time <span class="Normal">/</span> <span class="javaScriptNumber">4000</span><span class="Normal">)</span><span class="Normal">,</span>
      <span class="Structure">Math</span>.<span class="Keyword">cos</span><span class="Normal">(</span>time <span class="Normal">/</span> <span class="javaScriptNumber">6000</span><span class="Normal">)</span><span class="Normal">,</span>
      <span class="javaScriptNumber">1.0</span>
    <span class="Normal">)</span>

    <span class="Comment">// Clear the framebuffer</span>
    gl.<span class="Keyword">clear</span><span class="Normal">(</span>gl.COLOR_BUFFER_BIT <span class="Normal">|</span> gl.DEPTH_BUFFER_BIT<span class="Normal">)</span>

    <span class="Comment">// Normally you'd loop through each of the views reported by the frame</span>
    <span class="Comment">// and draw them into the corresponding viewport here, but we're</span>
    <span class="Comment">// keeping this sample slim so we're not bothering to draw any</span>
    <span class="Comment">// geometry.</span>
    <span class="Comment">/* for (let view of frame.views) {</span>
<span class="Comment">            let viewport = session.baseLayer.getViewport(view);</span>
<span class="Comment">            gl.viewport(viewport.x, viewport.y,</span>
<span class="Comment">                        viewport.width, viewport.height);</span>
<span class="Comment">            // Draw something.</span>
<span class="Comment">          } */</span>
  <span class="javaScriptBraces">}</span>
<span class="javaScriptBraces">}</span>

<span class="Comment">// Start the XR application.</span>
initXR<span class="Normal">()</span>
</pre>
</body>
</html>
<!-- vim: set foldmethod=manual : -->

dist.css

*{box-sizing:border-box}

dist.js

!function(e){function n(t){if(l[t])return l[t].exports;var c=l[t]={i:t,l:!1,exports:{}};return e[t].call(c.exports,c,c.exports,n),c.l=!0,c.exports}var l={};n.m=e,n.c=l,n.i=function(e){return e},n.d=function(e,l,t){n.o(e,l)||Object.defineProperty(e,l,{configurable:!1,enumerable:!0,get:t})},n.n=function(e){var l=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(l,"a",l),l},n.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},n.p="",n(n.s=2)}([function(module,exports,__webpack_require__){"use strict";eval("\n\nvar _log = __webpack_require__(1);\n\nvar _log2 = _interopRequireDefault(_log);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar log = (0, _log2.default)();\n\n// XR globals.\nvar xrButton = document.getElementById('xr-button');\nvar xrDevice = null;\nvar xrSession = null;\nvar xrFrameOfRef = null;\n\n// WebGL scene globals.\nvar gl = null;\n\n// Checks to see if WebXR is available and, if so, requests an XRDevice\n// that is connected to the system and tests it to ensure it supports the\n// desired session options.\nfunction initXR() {\n  // Is WebXR available on this UA?\n  if (navigator.xr) {\n    // Request an XRDevice connected to the system.\n    navigator.xr.requestDevice().then(function (device) {\n      xrDevice = device;\n      // If the device allows creation of exclusive sessions,\n      // set it as the target of the 'Enter XR' button.\n      device.supportsSession({ exclusive: true }).then(function () {\n        // Updates the button to start an XR session when clicked.\n        xrButton.addEventListener('click', onButtonClicked);\n        xrButton.innerHTML = 'Enter XR';\n        xrButton.disabled = false;\n      });\n    });\n  } else {\n    log.message('This browser does not support the WebXR API.');\n  }\n}\n\n// Called when the user clicks the button to enter XR. If we don't have a\n// session already we'll request one, and if we do we'll end it.\nfunction onButtonClicked() {\n  if (!xrSession) {\n    xrDevice.requestSession({ exclusive: true }).then(onSessionStarted);\n  } else {\n    xrSession.end();\n  }\n}\n\n// Called when we've successfully acquired a XRSession. In response we\n// will set up the necessary session state and kick off the frame loop.\nfunction onSessionStarted(session) {\n  // Save session to global.\n  xrSession = session;\n  xrButton.innerHTML = 'Exit XR';\n\n  // Listen for the sessions 'end' event so we can respond if the user\n  // or UA ends the session for any reason.\n  session.addEventListener('end', onSessionEnded);\n\n  // Create a WebGL context to render with, initialized to be compatible\n  // with the XRDisplay we're presenting to.\n  var canvas = document.createElement('canvas');\n  gl = canvas.getContext('webgl', {\n    compatibleXRDevice: session.device\n  });\n\n  // Use the new WebGL context to create a XRWebGLLayer and set it as the\n  // sessions baseLayer. This allows any content rendered to the layer to\n  // be displayed on the XRDevice.\n  session.baseLayer = new window.XRWebGLLayer(session, gl);\n\n  // Get a frame of reference, which is required for querying poses. In\n  // this case an 'eyeLevel' frame of reference means that all poses will\n  // be relative to the location where the XRDevice was first detected.\n  session.requestFrameOfReference('eyeLevel').then(function (frameOfRef) {\n    xrFrameOfRef = frameOfRef;\n    // Inform the session that we're ready to begin drawing.\n    session.requestAnimationFrame(onXRFrame);\n  });\n}\n\n// // // Called when the user clicks the 'Exit XR' button. In response we end\n// // // the session.\n// // function onEndSession (session) {\n// //   session.end()\n// // }\n\n// Called either when the user has explicitly ended the session (like in\n// onEndSession()) or when the UA has ended the session for any reason.\n// At this point the session object is no longer usable and should be discarded.\nfunction onSessionEnded(event) {\n  xrSession = null;\n  xrButton.innerHTML = 'Enter VR';\n  // In this simple case discard the WebGL context too, since we're not\n  // rendering anything else to the screen with it.\n  gl = null;\n}\n\n// Called every time the XRSession requests that a new frame be drawn.\nfunction onXRFrame(t, frame) {\n  var session = frame.session;\n\n  // Inform the session that we're ready for the next frame.\n  session.requestAnimationFrame(onXRFrame);\n\n  // Get the XRDevice pose relative to the Frame of Reference we created\n  // earlier.\n  var pose = frame.getDevicePose(xrFrameOfRef);\n\n  // Getting the pose may fail if, for example, tracking is lost. So we\n  // have to check to make sure that we got a valid pose before attempting\n  // to render with it. If not in this case we'll just leave the\n  // framebuffer cleared, so tracking loss means the scene will simply\n  // dissapear.\n  if (pose) {\n    // If we do have a valid pose, bind the WebGL layer's framebuffer,\n    // which is where any content to be displayed on the XRDevice must be\n    // rendered.\n    gl.bindFramebuffer(gl.FRAMEBUFFER, session.baseLayer.framebuffer);\n\n    // Update the clear color so that we can observe the color in the\n    // headset changing over time.\n    var time = Date.now();\n    gl.clearColor(Math.cos(time / 2000), Math.cos(time / 4000), Math.cos(time / 6000), 1.0);\n\n    // Clear the framebuffer\n    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);\n\n    // Normally you'd loop through each of the views reported by the frame\n    // and draw them into the corresponding viewport here, but we're\n    // keeping this sample slim so we're not bothering to draw any\n    // geometry.\n    /* for (let view of frame.views) {\n            let viewport = session.baseLayer.getViewport(view);\n            gl.viewport(viewport.x, viewport.y,\n                        viewport.width, viewport.height);\n            // Draw something.\n          } */\n  }\n}\n\n// Start the XR application.\ninitXR();//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9kZW1vLmpzPzk2MDAiXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IExvZyBmcm9tICcuL2xvZy5qcydcbmNvbnN0IGxvZyA9IExvZygpXG5cbi8vIFhSIGdsb2JhbHMuXG5sZXQgeHJCdXR0b24gPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgneHItYnV0dG9uJylcbmxldCB4ckRldmljZSA9IG51bGxcbmxldCB4clNlc3Npb24gPSBudWxsXG5sZXQgeHJGcmFtZU9mUmVmID0gbnVsbFxuXG4vLyBXZWJHTCBzY2VuZSBnbG9iYWxzLlxubGV0IGdsID0gbnVsbFxuXG4vLyBDaGVja3MgdG8gc2VlIGlmIFdlYlhSIGlzIGF2YWlsYWJsZSBhbmQsIGlmIHNvLCByZXF1ZXN0cyBhbiBYUkRldmljZVxuLy8gdGhhdCBpcyBjb25uZWN0ZWQgdG8gdGhlIHN5c3RlbSBhbmQgdGVzdHMgaXQgdG8gZW5zdXJlIGl0IHN1cHBvcnRzIHRoZVxuLy8gZGVzaXJlZCBzZXNzaW9uIG9wdGlvbnMuXG5mdW5jdGlvbiBpbml0WFIgKCkge1xuICAvLyBJcyBXZWJYUiBhdmFpbGFibGUgb24gdGhpcyBVQT9cbiAgaWYgKG5hdmlnYXRvci54cikge1xuICAgIC8vIFJlcXVlc3QgYW4gWFJEZXZpY2UgY29ubmVjdGVkIHRvIHRoZSBzeXN0ZW0uXG4gICAgbmF2aWdhdG9yLnhyLnJlcXVlc3REZXZpY2UoKS50aGVuKGRldmljZSA9PiB7XG4gICAgICB4ckRldmljZSA9IGRldmljZVxuICAgICAgLy8gSWYgdGhlIGRldmljZSBhbGxvd3MgY3JlYXRpb24gb2YgZXhjbHVzaXZlIHNlc3Npb25zLFxuICAgICAgLy8gc2V0IGl0IGFzIHRoZSB0YXJnZXQgb2YgdGhlICdFbnRlciBYUicgYnV0dG9uLlxuICAgICAgZGV2aWNlLnN1cHBvcnRzU2Vzc2lvbih7IGV4Y2x1c2l2ZTogdHJ1ZSB9KS50aGVuKCgpID0+IHtcbiAgICAgICAgLy8gVXBkYXRlcyB0aGUgYnV0dG9uIHRvIHN0YXJ0IGFuIFhSIHNlc3Npb24gd2hlbiBjbGlja2VkLlxuICAgICAgICB4ckJ1dHRvbi5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIG9uQnV0dG9uQ2xpY2tlZClcbiAgICAgICAgeHJCdXR0b24uaW5uZXJIVE1MID0gJ0VudGVyIFhSJ1xuICAgICAgICB4ckJ1dHRvbi5kaXNhYmxlZCA9IGZhbHNlXG4gICAgICB9KVxuICAgIH0pXG4gIH0gZWxzZSB7XG4gICAgbG9nLm1lc3NhZ2UoJ1RoaXMgYnJvd3NlciBkb2VzIG5vdCBzdXBwb3J0IHRoZSBXZWJYUiBBUEkuJylcbiAgfVxufVxuXG4vLyBDYWxsZWQgd2hlbiB0aGUgdXNlciBjbGlja3MgdGhlIGJ1dHRvbiB0byBlbnRlciBYUi4gSWYgd2UgZG9uJ3QgaGF2ZSBhXG4vLyBzZXNzaW9uIGFscmVhZHkgd2UnbGwgcmVxdWVzdCBvbmUsIGFuZCBpZiB3ZSBkbyB3ZSdsbCBlbmQgaXQuXG5mdW5jdGlvbiBvbkJ1dHRvbkNsaWNrZWQgKCkge1xuICBpZiAoIXhyU2Vzc2lvbikge1xuICAgIHhyRGV2aWNlLnJlcXVlc3RTZXNzaW9uKHsgZXhjbHVzaXZlOiB0cnVlIH0pLnRoZW4ob25TZXNzaW9uU3RhcnRlZClcbiAgfSBlbHNlIHtcbiAgICB4clNlc3Npb24uZW5kKClcbiAgfVxufVxuXG4vLyBDYWxsZWQgd2hlbiB3ZSd2ZSBzdWNjZXNzZnVsbHkgYWNxdWlyZWQgYSBYUlNlc3Npb24uIEluIHJlc3BvbnNlIHdlXG4vLyB3aWxsIHNldCB1cCB0aGUgbmVjZXNzYXJ5IHNlc3Npb24gc3RhdGUgYW5kIGtpY2sgb2ZmIHRoZSBmcmFtZSBsb29wLlxuZnVuY3Rpb24gb25TZXNzaW9uU3RhcnRlZCAoc2Vzc2lvbikge1xuICAvLyBTYXZlIHNlc3Npb24gdG8gZ2xvYmFsLlxuICB4clNlc3Npb24gPSBzZXNzaW9uXG4gIHhyQnV0dG9uLmlubmVySFRNTCA9ICdFeGl0IFhSJ1xuXG4gIC8vIExpc3RlbiBmb3IgdGhlIHNlc3Npb25zICdlbmQnIGV2ZW50IHNvIHdlIGNhbiByZXNwb25kIGlmIHRoZSB1c2VyXG4gIC8vIG9yIFVBIGVuZHMgdGhlIHNlc3Npb24gZm9yIGFueSByZWFzb24uXG4gIHNlc3Npb24uYWRkRXZlbnRMaXN0ZW5lcignZW5kJywgb25TZXNzaW9uRW5kZWQpXG5cbiAgLy8gQ3JlYXRlIGEgV2ViR0wgY29udGV4dCB0byByZW5kZXIgd2l0aCwgaW5pdGlhbGl6ZWQgdG8gYmUgY29tcGF0aWJsZVxuICAvLyB3aXRoIHRoZSBYUkRpc3BsYXkgd2UncmUgcHJlc2VudGluZyB0by5cbiAgbGV0IGNhbnZhcyA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2NhbnZhcycpXG4gIGdsID0gY2FudmFzLmdldENvbnRleHQoJ3dlYmdsJywge1xuICAgIGNvbXBhdGlibGVYUkRldmljZTogc2Vzc2lvbi5kZXZpY2VcbiAgfSlcblxuICAvLyBVc2UgdGhlIG5ldyBXZWJHTCBjb250ZXh0IHRvIGNyZWF0ZSBhIFhSV2ViR0xMYXllciBhbmQgc2V0IGl0IGFzIHRoZVxuICAvLyBzZXNzaW9ucyBiYXNlTGF5ZXIuIFRoaXMgYWxsb3dzIGFueSBjb250ZW50IHJlbmRlcmVkIHRvIHRoZSBsYXllciB0b1xuICAvLyBiZSBkaXNwbGF5ZWQgb24gdGhlIFhSRGV2aWNlLlxuICBzZXNzaW9uLmJhc2VMYXllciA9IG5ldyB3aW5kb3cuWFJXZWJHTExheWVyKHNlc3Npb24sIGdsKVxuXG4gIC8vIEdldCBhIGZyYW1lIG9mIHJlZmVyZW5jZSwgd2hpY2ggaXMgcmVxdWlyZWQgZm9yIHF1ZXJ5aW5nIHBvc2VzLiBJblxuICAvLyB0aGlzIGNhc2UgYW4gJ2V5ZUxldmVsJyBmcmFtZSBvZiByZWZlcmVuY2UgbWVhbnMgdGhhdCBhbGwgcG9zZXMgd2lsbFxuICAvLyBiZSByZWxhdGl2ZSB0byB0aGUgbG9jYXRpb24gd2hlcmUgdGhlIFhSRGV2aWNlIHdhcyBmaXJzdCBkZXRlY3RlZC5cbiAgc2Vzc2lvbi5yZXF1ZXN0RnJhbWVPZlJlZmVyZW5jZSgnZXllTGV2ZWwnKS50aGVuKGZyYW1lT2ZSZWYgPT4ge1xuICAgIHhyRnJhbWVPZlJlZiA9IGZyYW1lT2ZSZWZcbiAgICAvLyBJbmZvcm0gdGhlIHNlc3Npb24gdGhhdCB3ZSdyZSByZWFkeSB0byBiZWdpbiBkcmF3aW5nLlxuICAgIHNlc3Npb24ucmVxdWVzdEFuaW1hdGlvbkZyYW1lKG9uWFJGcmFtZSlcbiAgfSlcbn1cblxuLy8gLy8gLy8gQ2FsbGVkIHdoZW4gdGhlIHVzZXIgY2xpY2tzIHRoZSAnRXhpdCBYUicgYnV0dG9uLiBJbiByZXNwb25zZSB3ZSBlbmRcbi8vIC8vIC8vIHRoZSBzZXNzaW9uLlxuLy8gLy8gZnVuY3Rpb24gb25FbmRTZXNzaW9uIChzZXNzaW9uKSB7XG4vLyAvLyAgIHNlc3Npb24uZW5kKClcbi8vIC8vIH1cblxuLy8gQ2FsbGVkIGVpdGhlciB3aGVuIHRoZSB1c2VyIGhhcyBleHBsaWNpdGx5IGVuZGVkIHRoZSBzZXNzaW9uIChsaWtlIGluXG4vLyBvbkVuZFNlc3Npb24oKSkgb3Igd2hlbiB0aGUgVUEgaGFzIGVuZGVkIHRoZSBzZXNzaW9uIGZvciBhbnkgcmVhc29uLlxuLy8gQXQgdGhpcyBwb2ludCB0aGUgc2Vzc2lvbiBvYmplY3QgaXMgbm8gbG9uZ2VyIHVzYWJsZSBhbmQgc2hvdWxkIGJlIGRpc2NhcmRlZC5cbmZ1bmN0aW9uIG9uU2Vzc2lvbkVuZGVkIChldmVudCkge1xuICB4clNlc3Npb24gPSBudWxsXG4gIHhyQnV0dG9uLmlubmVySFRNTCA9ICdFbnRlciBWUidcbiAgLy8gSW4gdGhpcyBzaW1wbGUgY2FzZSBkaXNjYXJkIHRoZSBXZWJHTCBjb250ZXh0IHRvbywgc2luY2Ugd2UncmUgbm90XG4gIC8vIHJlbmRlcmluZyBhbnl0aGluZyBlbHNlIHRvIHRoZSBzY3JlZW4gd2l0aCBpdC5cbiAgZ2wgPSBudWxsXG59XG5cbi8vIENhbGxlZCBldmVyeSB0aW1lIHRoZSBYUlNlc3Npb24gcmVxdWVzdHMgdGhhdCBhIG5ldyBmcmFtZSBiZSBkcmF3bi5cbmZ1bmN0aW9uIG9uWFJGcmFtZSAodCwgZnJhbWUpIHtcbiAgbGV0IHNlc3Npb24gPSBmcmFtZS5zZXNzaW9uXG5cbiAgLy8gSW5mb3JtIHRoZSBzZXNzaW9uIHRoYXQgd2UncmUgcmVhZHkgZm9yIHRoZSBuZXh0IGZyYW1lLlxuICBzZXNzaW9uLnJlcXVlc3RBbmltYXRpb25GcmFtZShvblhSRnJhbWUpXG5cbiAgLy8gR2V0IHRoZSBYUkRldmljZSBwb3NlIHJlbGF0aXZlIHRvIHRoZSBGcmFtZSBvZiBSZWZlcmVuY2Ugd2UgY3JlYXRlZFxuICAvLyBlYXJsaWVyLlxuICBsZXQgcG9zZSA9IGZyYW1lLmdldERldmljZVBvc2UoeHJGcmFtZU9mUmVmKVxuXG4gIC8vIEdldHRpbmcgdGhlIHBvc2UgbWF5IGZhaWwgaWYsIGZvciBleGFtcGxlLCB0cmFja2luZyBpcyBsb3N0LiBTbyB3ZVxuICAvLyBoYXZlIHRvIGNoZWNrIHRvIG1ha2Ugc3VyZSB0aGF0IHdlIGdvdCBhIHZhbGlkIHBvc2UgYmVmb3JlIGF0dGVtcHRpbmdcbiAgLy8gdG8gcmVuZGVyIHdpdGggaXQuIElmIG5vdCBpbiB0aGlzIGNhc2Ugd2UnbGwganVzdCBsZWF2ZSB0aGVcbiAgLy8gZnJhbWVidWZmZXIgY2xlYXJlZCwgc28gdHJhY2tpbmcgbG9zcyBtZWFucyB0aGUgc2NlbmUgd2lsbCBzaW1wbHlcbiAgLy8gZGlzc2FwZWFyLlxuICBpZiAocG9zZSkge1xuICAgIC8vIElmIHdlIGRvIGhhdmUgYSB2YWxpZCBwb3NlLCBiaW5kIHRoZSBXZWJHTCBsYXllcidzIGZyYW1lYnVmZmVyLFxuICAgIC8vIHdoaWNoIGlzIHdoZXJlIGFueSBjb250ZW50IHRvIGJlIGRpc3BsYXllZCBvbiB0aGUgWFJEZXZpY2UgbXVzdCBiZVxuICAgIC8vIHJlbmRlcmVkLlxuICAgIGdsLmJpbmRGcmFtZWJ1ZmZlcihnbC5GUkFNRUJVRkZFUiwgc2Vzc2lvbi5iYXNlTGF5ZXIuZnJhbWVidWZmZXIpXG5cbiAgICAvLyBVcGRhdGUgdGhlIGNsZWFyIGNvbG9yIHNvIHRoYXQgd2UgY2FuIG9ic2VydmUgdGhlIGNvbG9yIGluIHRoZVxuICAgIC8vIGhlYWRzZXQgY2hhbmdpbmcgb3ZlciB0aW1lLlxuICAgIGxldCB0aW1lID0gRGF0ZS5ub3coKVxuICAgIGdsLmNsZWFyQ29sb3IoXG4gICAgICBNYXRoLmNvcyh0aW1lIC8gMjAwMCksXG4gICAgICBNYXRoLmNvcyh0aW1lIC8gNDAwMCksXG4gICAgICBNYXRoLmNvcyh0aW1lIC8gNjAwMCksXG4gICAgICAxLjBcbiAgICApXG5cbiAgICAvLyBDbGVhciB0aGUgZnJhbWVidWZmZXJcbiAgICBnbC5jbGVhcihnbC5DT0xPUl9CVUZGRVJfQklUIHwgZ2wuREVQVEhfQlVGRkVSX0JJVClcblxuICAgIC8vIE5vcm1hbGx5IHlvdSdkIGxvb3AgdGhyb3VnaCBlYWNoIG9mIHRoZSB2aWV3cyByZXBvcnRlZCBieSB0aGUgZnJhbWVcbiAgICAvLyBhbmQgZHJhdyB0aGVtIGludG8gdGhlIGNvcnJlc3BvbmRpbmcgdmlld3BvcnQgaGVyZSwgYnV0IHdlJ3JlXG4gICAgLy8ga2VlcGluZyB0aGlzIHNhbXBsZSBzbGltIHNvIHdlJ3JlIG5vdCBib3RoZXJpbmcgdG8gZHJhdyBhbnlcbiAgICAvLyBnZW9tZXRyeS5cbiAgICAvKiBmb3IgKGxldCB2aWV3IG9mIGZyYW1lLnZpZXdzKSB7XG4gICAgICAgICAgICBsZXQgdmlld3BvcnQgPSBzZXNzaW9uLmJhc2VMYXllci5nZXRWaWV3cG9ydCh2aWV3KTtcbiAgICAgICAgICAgIGdsLnZpZXdwb3J0KHZpZXdwb3J0LngsIHZpZXdwb3J0LnksXG4gICAgICAgICAgICAgICAgICAgICAgICB2aWV3cG9ydC53aWR0aCwgdmlld3BvcnQuaGVpZ2h0KTtcbiAgICAgICAgICAgIC8vIERyYXcgc29tZXRoaW5nLlxuICAgICAgICAgIH0gKi9cbiAgfVxufVxuXG4vLyBTdGFydCB0aGUgWFIgYXBwbGljYXRpb24uXG5pbml0WFIoKVxuXG5cblxuLy8gV0VCUEFDSyBGT09URVIgLy9cbi8vIGRlbW8uanMiXSwibWFwcGluZ3MiOiI7O0FBQUE7QUFDQTs7Ozs7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFEQTtBQUNBO0FBR0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQU1BO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7Ozs7OztBQU1BO0FBQ0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///0\n")},function(module,exports,__webpack_require__){"use strict";eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nvar log = function log() {\n  window.logMessages = [];\n\n  var print = function print() {\n    document.querySelector('.logs').innerHTML = window.logMessages.slice(-10).map(function (d) {\n      return '<li>' + d.s + ' ' + (d.e ? d.e : '') + '</li>';\n    }).join('');\n  };\n\n  return {\n    message: function message(s) {\n      window.logMessages.push({ s: s });\n      print();\n    },\n    error: function error(s, e) {\n      window.logMessages.push({ s: s, e: e });\n      print();\n    }\n  };\n};\n\nexports.default = log;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMS5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9sb2cuanM/YjgzZiJdLCJzb3VyY2VzQ29udGVudCI6WyJjb25zdCBsb2cgPSAoKSA9PiB7XG4gIHdpbmRvdy5sb2dNZXNzYWdlcyA9IFtdXG5cbiAgY29uc3QgcHJpbnQgPSAoKSA9PiB7XG4gICAgZG9jdW1lbnQucXVlcnlTZWxlY3RvcignLmxvZ3MnKS5pbm5lckhUTUwgPSB3aW5kb3cubG9nTWVzc2FnZXNcbiAgICAgIC5zbGljZSgtMTApXG4gICAgICAubWFwKGQgPT4ge1xuICAgICAgICByZXR1cm4gYDxsaT4ke2Quc30gJHtkLmUgPyBkLmUgOiAnJ308L2xpPmBcbiAgICAgIH0pXG4gICAgICAuam9pbignJylcbiAgfVxuXG4gIHJldHVybiB7XG4gICAgbWVzc2FnZSAocykge1xuICAgICAgd2luZG93LmxvZ01lc3NhZ2VzLnB1c2goeyBzIH0pXG4gICAgICBwcmludCgpXG4gICAgfSxcblxuICAgIGVycm9yIChzLCBlKSB7XG4gICAgICB3aW5kb3cubG9nTWVzc2FnZXMucHVzaCh7IHMsIGUgfSlcbiAgICAgIHByaW50KClcbiAgICB9XG4gIH1cbn1cblxuZXhwb3J0IGRlZmF1bHQgbG9nXG5cblxuXG4vLyBXRUJQQUNLIEZPT1RFUiAvL1xuLy8gbG9nLmpzIl0sIm1hcHBpbmdzIjoiOzs7OztBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFHQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQVRBO0FBV0E7QUFDQTtBQUNBIiwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///1\n")},function(module,exports,__webpack_require__){"use strict";eval("\n\n__webpack_require__(0);//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMi5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9zY3JpcHQuanM/OWE5NSJdLCJzb3VyY2VzQ29udGVudCI6WyJyZXF1aXJlKCcuL2RlbW8uanMnKVxuXG5cblxuLy8gV0VCUEFDSyBGT09URVIgLy9cbi8vIHNjcmlwdC5qcyJdLCJtYXBwaW5ncyI6Ijs7QUFBQSIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2\n")}]);

log.js

const log = () => {
  window.logMessages = []

  const print = () => {
    document.querySelector('.logs').innerHTML = window.logMessages
      .slice(-10)
      .map(d => {
        return `<li>${d.s} ${d.e ? d.e : ''}</li>`
      })
      .join('')
  }

  return {
    message (s) {
      window.logMessages.push({ s })
      print()
    },

    error (s, e) {
      window.logMessages.push({ s, e })
      print()
    }
  }
}

export default log

main.js

import Log from './log.js'
import OnDrawFrame from './onDrawFrame.js'

const log = Log()

// TODO: review async functions
// TODO: check correct error handling setup

let device
let session
let frameOfRef
let onDrawFrame
let glCanvas
let gl

// 1 - Request an XR device.
// 2 - If a device is available, application advertises XR functionality to the user.
// 3 - Request an exclusive XR session from the device in response to a user-activation event.
// 4 - Setup necessary session state and kick off the frame loop.
// 5 - Use the session to run a render loop that produces graphical frames to be displayed on the XR device.
// 6 - Continue producing frames until the user indicates that they wish to exit XR mode.
// 7 - End the XR session.

// 3 - Request an exclusive XR session from the device in response to a user-activation event.
const beginXRSession = () => {
  device
    .requestSession({ exclusive: true })
    .then(xrSession => {
      log.message('session started')
      session = xrSession
      onSessionStarted()
      // Use a WebGL context as a base layer.
      // xrSession.baseLayer = new XRWebGLLayer(session, gl);
      // Start the render loop
    })
    .catch(err => {
      log.error('Could not request desired session', err)
    })
}

// 4 - Setup necessary session state and kick off the frame loop.
const onSessionStarted = () => {
  // Get a frame of reference, which is required for querying poses. In
  // this case an 'eyeLevel' frame of reference means that all poses will
  // be relative to the location where the XRDevice was first detected.
  session
    .requestFrameOfReference('eyeLevel')
    .then(xrFrameOfRef => {
      log.message('frame of ref acquired')
      frameOfRef = xrFrameOfRef
    })
    .then(setupWebGLLayer) // Create a compatible XRWebGLLayer.
    .then(() => {
      // Start the render loop.
      onDrawFrame = OnDrawFrame({ session, frameOfRef, gl, glCanvas, log })
      session.requestAnimationFrame(onDrawFrame)
    })
    .catch(err => {
      log.error('Error onSessionStarted', err)
    })
}

const setupWebGLLayer = () => {
  glCanvas = document.createElement('canvas')
  gl = glCanvas.getContext('webgl')

  // Make sure the canvas context we want to use is compatible with the device.
  return gl
    .setCompatibleXRDevice(device)
    .then(() => {
      log.message('setting baseLayer')
      // The content that will be shown on the device
      // is defined by the session's baseLayer.
      session.baseLayer = new window.XRWebGLLayer(session, gl)
    })
    .catch(err => {
      log.error('Error setupWebGLLayer', err)
    })
}

if (navigator.xr) {
  //
  // 1 - Request an XR device.
  //
  navigator.xr
    .requestDevice()
    .then(xrDevice => {
      xrDevice
        .supportsSession({ exclusive: true })
        .then(() => {
          //
          // 2 - If a device is available, application advertises XR functionality to the user.
          //
          device = xrDevice
          const button = document.createElement('button')
          button.innerHTML = 'Enter VR'
          button.addEventListener('click', beginXRSession)
          document.body.appendChild(button)
        })
        .catch(err => {
          log.error('Could not support desired session', err)
        })
    })
    .catch(err => {
      if (err.name === 'NotFoundError') {
        // No XRDevices available.
        log.error('No XR devices available:', err)
      } else {
        // An error occurred while requesting an XRDevice.
        log.error('Requesting XR device failed:', err)
      }
    })
} else {
  log.message('This browser does not support the WebXR API.')
}

onDrawFrame.js

const onDrawFrame = ({ session, frameOfRef, gl, glCanvas, log }) => (
  timestamp,
  xrFrame
) => {
  // Do we have an active session?
  if (session) {
    log.message('found active session')
    let pose = xrFrame.getDevicePose(frameOfRef)
    gl.bindFramebuffer(session.baseLayer.framebuffer)

    for (let view of xrFrame.views) {
      let viewport = session.baseLayer.getViewport(view)
      gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height)
      log.message('drawing gl viewport')
      drawScene(view, pose)
    }

    // Request the next animation callback
    // session.requestAnimationFrame(onDrawFrame)
  } else {
    // No session available, so render a default mono view.
    gl.viewport(0, 0, glCanvas.width, glCanvas.height)
    // drawScene()

    // Request the next window callback
    window.requestAnimationFrame(onDrawFrame)
  }
}

const drawScene = (view, pose) => {
  let viewMatrix = null
  let projectionMatrix = null
  if (view) {
    viewMatrix = pose.getViewMatrix(view)
    projectionMatrix = view.projectionMatrix
  } else {
    viewMatrix = defaultViewMatrix
    projectionMatrix = defaultProjectionMatrix
  }

  // Set uniforms as appropriate for shaders being used

  // Draw Scene
}

export default onDrawFrame

package.json

{
  "standard": {
    "globals": [
    ]
  }
}

style.styl

*
  box-sizing border-box

test.js

let xrDevice = null

async function onXRAvailable (device) {
  xrDevice = device

  // Most (but not all) XRDevices are capable of granting exclusive access to
  // the device, which is necessary to show imagery in a headset. If the device
  // has that capability the page will want to add an "Enter VR" button (similar
  // to "Enter Fullscreen") that triggers the page to begin showing imagery on
  // the headset.
  xrDevice
    .supportsSession({ exclusive: true })
    .then(() => {
      var enterXrBtn = document.createElement('button')
      enterXrBtn.innerHTML = 'Enter VR'
      enterXrBtn.addEventListener('click', beginXRSession)
      document.body.appendChild(enterVrBtn)
    })
    .catch(reason => {
      console.log('Session not supported: ' + reason)
    })
}