<div id="page_container">
  <div id="canvas_container">
    <div class="banner"><div class="text">Device is presenting</div></div>
    <canvas id="webglCanvas"></canvas>
  </div>

  <div class="buttons">
    <button class="presentation-hide" onclick="tryRequestPresent()">Request Present & Pointerlock</button> 
    <button class="presentation-show" onclick="tryExitPresent()">Exit Present</button>
    <button class="presentation-show" onclick="webglCanvas.requestPointerLock()">Request Pointerlock</button>
  </div>

  <div class="action-source">Current Action Source: <span id="state"></span></div>  

  <h3>Log:</h3>
  <div id="log"></div>
</div>
.default { color: #999; }
.pointer { color: #F00; }
.keyboard { color: #00F; }
.gamepad { color: #090; }

div.action-source { display: inline-block; padding: 10px;}
body, html {
  width: 100%;  
  height: 100%; 
  margin: 0;
  font-family: segoe ui;
}

h3 {
  font-size: 20px;
}

div {
  font-size: 12px;
}

.fail {
  word-wrap: break-word;
  color: red;
  font-weight: bold;
} 
.note {
  word-wrap: break-word;
  color: blue;
  font-weight: bold;
} 
#canvas_container {
  height: 200px;
  width: 400px;
  float: right;
}
#canvas_container canvas {
  height: 100%;
  width: 100%;
}
#canvas_container .banner {
  display: none;
  height: 200px;
  width: 400px;
  background: grey;
  position: absolute;
}
#canvas_container .banner .text {
  color: white;
  font-weight: bold;
  padding: 15px;
}
#page_container.presenting.external .banner {
  display: inherit;  
}
#page_container.presenting .presentation-hide {
  display: none;  
}
#page_container .presentation-show {
  display: none;  
}
#page_container.presenting .presentation-show {
  display: inline;  
}
window.onerror = function (msg, url, lineNo, columnNo, error) {  
  displayNote(msg); return false;
}

var webglCanvas = document.getElementById("webglCanvas"),
    gl = webglCanvas.getContext("experimental-webgl"),
    animationFrameId,
    vrDisplay = null,
    myAction,
    pulse = 0;

function init() {
  
  // Defines what happens when user input is detected.
  myAction = new Action(
    function(source, active) { 
      // This is executed on any mouse button down, gamepad button down, or keyboard 'space' key down. 
      setState(active ? source : null); 
    });
  setState('');
  
  // Register for mouse restriction events, so we can request pointerlock.
  // See https://codepen.io/ransico/pen/dRXxVY for more details on this pointerlock implementation
  window.addEventListener('vrdisplaypointerrestricted', () => {
      displayNote('vrdisplaypointerrestricted'); 
      webglCanvas.requestPointerLock();
    }, false);
  window.addEventListener('vrdisplaypointerunrestricted', () => document.exitPointerLock(), false);  
  window.addEventListener('vrdisplaypresentchange', () => { if (!vrDisplay.isPresenting) document.exitPointerLock() }, false);
  
  // Register for mouse (any button) event handlers
  webglCanvas.addEventListener('pointerdown', () => myAction.activate('pointer'));
  webglCanvas.addEventListener('pointerup', () => myAction.deactivate('pointer'));
  // Register for keyboard (space bar) 
  window.addEventListener("keydown", (event) => { if (event.key === ' ') { myAction.activate('keyboard'); event.preventDefault(); }});
  window.addEventListener("keyup",   (event) => { if (event.key === ' ') { myAction.deactivate('keyboard'); event.preventDefault(); }});
}

// Simple helper function; simply checks if either button at index 0 or 1 is pressed on the given gamepad.
function isSelectButtonPressed(gamepad) {
  if (!gamepad.buttons || !gamepad.buttons.length) return false;
  if (gamepad.buttons[0].pressed) return true;
  if (gamepad.buttons.length > 1 && gamepad.buttons[1].pressed) return true;
  return false;
}

// This method is called one per frame, we need to read gamepad state here since the Gamepad API relies on Polling.
function tick() {
  // You should make a new call to getGamepads each frame. Here we use feature detection to gracefully handle absence of gamepad support.
  var gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
  var activate = false;
  for (var i = 0; i < gamepads.length; i++) {
    if (gamepads[i] && isSelectButtonPressed(gamepads[i])) {
      activate = true;
      break;
    }
  }

  if (activate) myAction.activate('gamepad'); 
  else myAction.deactivate('gamepad');
}


/*******************************************\
 * RENDER LOOP AND UTIL CLASSES BELOW HERE *
\*******************************************/

// A simple representation of an action. This keeps track of which input source 'started' the press, to prevent duplicate events being fired when pressing multiple inputs simultaniously (touch + mouse, for example)
function Action(callback) {
  this.callback = callback;
  this.activate = function(source) {
    if (this.active) { displayNote('ignored activate: ' + source); return };
    displayNote('activate: ' + source);
    this.active = source;

    // Do something in your scene
    callback(source, true);
  };
  this.deactivate = function(source) {
    if (source !== this.active) return;
    displayNote('deactivate: ' + source);
    this.active = null;

    // Do something in your scene
    callback(null, false);
  }
  this.active = null;
}

function setState(text) {
  text = text || 'deactivated';
  document.getElementById('state').innerHTML = "<span class=\"" + text + "\">" + text + "</span>";
}

function tryRequestPresent() {
  
    if (!vrDisplay) return displayRejection(null, "Please plug in an HMD");  
  
    // Part 2/2 of Fallback method for browsers that do not raise pointerrestricted events.
    if (typeof(window.onvrdisplaypointerrestricted) === 'undefined') {
        displayNote('requesting pointerlock in absense of window.vrdisplaypointerrestricted');
        webglCanvas.requestPointerLock();
    }

    vrDisplay.requestPresent([{source: webglCanvas}]).then(() => {
        displayNote('Entered VR on ' + vrDisplay.displayName);
    }, (e) => {
        displayRejection(e, "requestPresent rejected");
    });  
}

function onAnimationFrame() {

  draw();

  // Indicate that we are ready to present the rendered frame to the VRDisplay
  if (vrDisplay) { 
    vrDisplay.requestAnimationFrame(onAnimationFrame);

    if (vrDisplay.isPresenting) {
      vrDisplay.submitFrame();
    }
  } else {
    window.requestAnimationFrame(onAnimationFrame);
  }
  
  tick();
}

// Clear the screen color based on the action state.
var stateColors = {
  default: [0.5, 0.5, 0.5, 1.0],
  pointer: [1.0, 0.0, 0.0, 1.0],
  gamepad: [0.0, 1.0, 0.0, 1.0],
  keyboard: [0.0, 0.0, 1.0, 1.0]
};

function draw() {
  pulse  = ++pulse % 100;
  let val = Math.sin(pulse * 0.06283) * 0.25 + 0.75,
      color = stateColors[myAction.active || 'default'] || stateColors['default'];
  gl.clearColor(
    color[0] * val,
    color[1] * val,
    color[2] * val,
    color[3]);
  gl.clear(gl.COLOR_BUFFER_BIT);
}

function initVR() {  
  
  // Handle VRDisplay Detection
  if (navigator.getVRDisplays) {    
    // Connection events
    window.addEventListener('vrdisplayconnect', (event) => { 
      vrDisplay = event.display; 
      displayNote('VRDisplayConnect: ' + (event&&event.display?event.display.displayName:'Unknown'));
    }, false );
    window.addEventListener('vrdisplaydisconnect', (event) => vrdisplay = null, false);
    // Initial page load, is there a display?
    navigator.getVRDisplays().then((displays) => {
      if (displays.length !== 1) {
        return displayRejection(null, "getVRDisplays.length is 0 - plug in your HMD now.");
      }
      vrDisplay = displays[0];
    }, (e) => {
      displayRejection(e, "getVRDisplays rejected");
    });
  } else {
      displayRejection(null, "navigator.getVRDisplays not defined. Does your browser support WebVR?");
  }

  // Change some styles on presentation change
  window.addEventListener('vrdisplaypresentchange', (d) => {  
    let className = '';
    if (vrDisplay.isPresenting) {
      className += 'presenting';
      className += vrDisplay.capabilities.hasExternalDisplay ? ' external' : ' non-external';
    } else {
      className = '';
    }
    document.getElementById('page_container').className = className;
  });

  window.requestAnimationFrame(onAnimationFrame);
}

function tryExitPresent() {          
    vrDisplay && vrDisplay.exitPresent().then(() => displayNote('Exited VR.'), displayRejection);
}

function displayNote(message) {
    let elem = document.getElementById('log');
    elem.innerHTML = '<div class="note">' + message + '</div>' + elem.innerHTML;
}

function displayRejection(e, message) {
    let output = message + (e ? (': ' + (e.message || e)) : ''),
        elem = document.getElementById('log');
    elem.innerHTML = '<div class="fail">' + output + '</div>' + elem.innerHTML;
}

init();
initVR();

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.