Newer
Older
reroad-test / 2020-ryusei / aframe-master / src / core / scene / a-scene.js
@ryusei ryusei on 22 Oct 2020 30 KB パノラマ表示
/* global Promise, screen, CustomEvent */
var initMetaTags = require('./metaTags').inject;
var initWakelock = require('./wakelock');
var loadingScreen = require('./loadingScreen');
var re = require('../a-register-element');
var scenes = require('./scenes');
var systems = require('../system').systems;
var THREE = require('../../lib/three');
var utils = require('../../utils/');
// Require after.
var AEntity = require('../a-entity');
var ANode = require('../a-node');
var initPostMessageAPI = require('./postMessage');

var bind = utils.bind;
var isIOS = utils.device.isIOS();
var isMobile = utils.device.isMobile();
var isWebXRAvailable = utils.device.isWebXRAvailable;
var registerElement = re.registerElement;
var warn = utils.debug('core:a-scene:warn');

/**
 * Scene element, holds all entities.
 *
 * @member {array} behaviors - Component instances that have registered themselves to be
           updated on every tick.
 * @member {object} camera - three.js Camera object.
 * @member {object} canvas
 * @member {bool} isScene - Differentiates as scene entity as opposed to other entites.
 * @member {bool} isMobile - Whether browser is mobile (via UA detection).
 * @member {object} object3D - Root three.js Scene object.
 * @member {object} renderer
 * @member {bool} renderStarted
 * @member {object} systems - Registered instantiated systems.
 * @member {number} time
 */
module.exports.AScene = registerElement('a-scene', {
  prototype: Object.create(AEntity.prototype, {
    createdCallback: {
      value: function () {
        this.clock = new THREE.Clock();
        this.isIOS = isIOS;
        this.isMobile = isMobile;
        this.hasWebXR = isWebXRAvailable;
        this.isAR = false;
        this.isScene = true;
        this.object3D = new THREE.Scene();
        var self = this;
        this.object3D.onAfterRender = function (renderer, scene, camera) {
          // THREE may swap the camera used for the rendering if in VR, so we pass it to tock
          if (self.isPlaying) { self.tock(self.time, self.delta, camera); }
        };
        this.resize = bind(this.resize, this);
        this.render = bind(this.render, this);
        this.systems = {};
        this.systemNames = [];
        this.time = this.delta = 0;

        this.behaviors = {tick: [], tock: []};
        this.hasLoaded = false;
        this.isPlaying = false;
        this.originalHTML = this.innerHTML;

        // Default components.
        this.setAttribute('inspector', '');
        this.setAttribute('keyboard-shortcuts', '');
        this.setAttribute('screenshot', '');
        this.setAttribute('vr-mode-ui', '');
        this.setAttribute('device-orientation-permission-ui', '');
      }
    },

    addFullScreenStyles: {
      value: function () {
        document.documentElement.classList.add('a-fullscreen');
      }
    },

    removeFullScreenStyles: {
      value: function () {
        document.documentElement.classList.remove('a-fullscreen');
      }
    },

    attachedCallback: {
      value: function () {
        var self = this;
        var embedded = this.hasAttribute('embedded');
        // Renderer initialization
        setupCanvas(this);
        this.setupRenderer();

        this.resize();
        if (!embedded) { this.addFullScreenStyles(); }
        initPostMessageAPI(this);

        initMetaTags(this);
        initWakelock(this);

        // Handler to exit VR (e.g., Oculus Browser back button).
        this.onVRPresentChangeBound = bind(this.onVRPresentChange, this);
        window.addEventListener('vrdisplaypresentchange', this.onVRPresentChangeBound);

        // Bind functions.
        this.enterVRBound = function () { self.enterVR(); };
        this.exitVRBound = function () { self.exitVR(); };
        this.exitVRTrueBound = function () { self.exitVR(true); };
        this.pointerRestrictedBound = function () { self.pointerRestricted(); };
        this.pointerUnrestrictedBound = function () { self.pointerUnrestricted(); };

        if (!isWebXRAvailable) {
          // Exit VR on `vrdisplaydeactivate` (e.g. taking off Rift headset).
          window.addEventListener('vrdisplaydeactivate', this.exitVRBound);

          // Exit VR on `vrdisplaydisconnect` (e.g. unplugging Rift headset).
          window.addEventListener('vrdisplaydisconnect', this.exitVRTrueBound);

          // Register for mouse restricted events while in VR
          // (e.g. mouse no longer available on desktop 2D view)
          window.addEventListener('vrdisplaypointerrestricted', this.pointerRestrictedBound);

          // Register for mouse unrestricted events while in VR
          // (e.g. mouse once again available on desktop 2D view)
          window.addEventListener('vrdisplaypointerunrestricted',
                                  this.pointerUnrestrictedBound);
        }

        window.addEventListener('sessionend', this.resize);
        // Camera set up by camera system.
        this.addEventListener('cameraready', function () {
          self.attachedCallbackPostCamera();
        });

        this.initSystems();
      }
    },

    attachedCallbackPostCamera: {
      value: function () {
        var resize;
        var self = this;

        window.addEventListener('load', resize);
        window.addEventListener('resize', function () {
          // Workaround for a Webkit bug (https://bugs.webkit.org/show_bug.cgi?id=170595)
          // where the window does not contain the correct viewport size
          // after an orientation change. The window size is correct if the operation
          // is postponed a few milliseconds.
          // self.resize can be called directly once the bug above is fixed.
          if (self.isIOS) {
            setTimeout(self.resize, 100);
          } else {
            self.resize();
          }
        });
        this.play();

        // Add to scene index.
        scenes.push(this);
      },
      writable: window.debug
    },

    /**
     * Initialize all systems.
     */
    initSystems: {
      value: function () {
        var name;

        // Initialize camera system first.
        this.initSystem('camera');

        for (name in systems) {
          if (name === 'camera') { continue; }
          this.initSystem(name);
        }
      }
    },

    /**
     * Initialize a system.
     */
    initSystem: {
      value: function (name) {
        if (this.systems[name]) { return; }
        this.systems[name] = new systems[name](this);
        this.systemNames.push(name);
      }
    },

    /**
     * Shut down scene on detach.
     */
    detachedCallback: {
      value: function () {
        // Remove from scene index.
        var sceneIndex = scenes.indexOf(this);
        scenes.splice(sceneIndex, 1);

        window.removeEventListener('vrdisplaypresentchange', this.onVRPresentChangeBound);
        window.removeEventListener('vrdisplayactivate', this.enterVRBound);
        window.removeEventListener('vrdisplaydeactivate', this.exitVRBound);
        window.removeEventListener('vrdisplayconnect', this.enterVRBound);
        window.removeEventListener('vrdisplaydisconnect', this.exitVRTrueBound);
        window.removeEventListener('vrdisplaypointerrestricted', this.pointerRestrictedBound);
        window.removeEventListener('vrdisplaypointerunrestricted', this.pointerUnrestrictedBound);
        window.removeEventListener('sessionend', this.resize);
      }
    },

    /**
     * Add ticks and tocks.
     *
     * @param {object} behavior - A component.
     */
    addBehavior: {
      value: function (behavior) {
        var behaviorArr;
        var behaviors = this.behaviors;
        var behaviorType;

        // Check if behavior has tick and/or tock and add the behavior to the appropriate list.
        for (behaviorType in behaviors) {
          if (!behavior[behaviorType]) { continue; }
          behaviorArr = this.behaviors[behaviorType];
          if (behaviorArr.indexOf(behavior) === -1) {
            behaviorArr.push(behavior);
          }
        }
      }
    },

    /**
     * For tests.
     */
    getPointerLockElement: {
      value: function () {
        return document.pointerLockElement;
      },
      writable: window.debug
    },

    /**
     * For tests.
     */
    checkHeadsetConnected: {
      value: utils.device.checkHeadsetConnected,
      writable: window.debug
    },

    enterAR: {
      value: function () {
        var errorMessage;
        if (!this.hasWebXR) {
          errorMessage = 'Failed to enter AR mode, WebXR not supported.';
          throw new Error(errorMessage);
        }
        if (!utils.device.checkARSupport()) {
          errorMessage = 'Failed to enter AR, WebXR immersive-ar mode not supported in your browser or device.';
          throw new Error(errorMessage);
        }
        return this.enterVR(true);
      }
    },

    /**
     * Call `requestPresent` if WebVR or WebVR polyfill.
     * Call `requestFullscreen` on desktop.
     * Handle events, states, fullscreen styles.
     *
     * @param {bool?} useAR - if true, try immersive-ar mode
     * @returns {Promise}
     */
    enterVR: {
      value: function (useAR) {
        var self = this;
        var vrDisplay;
        var vrManager = self.renderer.xr;

        // Don't enter VR if already in VR.
        if (this.is('vr-mode')) { return Promise.resolve('Already in VR.'); }

        // Has VR.
        if (this.checkHeadsetConnected() || this.isMobile) {
          vrManager.enabled = true;

          if (this.hasWebXR) {
            // XR API.
            if (this.xrSession) {
              this.xrSession.removeEventListener('end', this.exitVRBound);
            }
            var refspace = this.sceneEl.systems.webxr.sessionReferenceSpaceType;
            vrManager.setReferenceSpaceType(refspace);
            var xrMode = useAR ? 'immersive-ar' : 'immersive-vr';
            var xrInit = this.sceneEl.systems.webxr.sessionConfiguration;
            return new Promise(function (resolve, reject) {
              navigator.xr.requestSession(xrMode, xrInit).then(
                function requestSuccess (xrSession) {
                  self.xrSession = xrSession;
                  vrManager.setSession(xrSession);
                  xrSession.addEventListener('end', self.exitVRBound);
                  if (useAR) { self.addState('ar-mode'); }
                  enterVRSuccess(resolve);
                },
                function requestFail (error) {
                  var useAR = xrMode === 'immersive-ar';
                  var mode = useAR ? 'AR' : 'VR';
                  throw new Error('Failed to enter ' + mode + ' mode (`requestSession`) ' + error);
                }
              );
            });
          } else {
            vrDisplay = utils.device.getVRDisplay();
            vrManager.setDevice(vrDisplay);
            if (vrDisplay.isPresenting &&
                !window.hasNativeWebVRImplementation) {
              enterVRSuccess();
              return Promise.resolve();
            }
            var rendererSystem = this.getAttribute('renderer');
            var presentationAttributes = {
              highRefreshRate: rendererSystem.highRefreshRate,
              foveationLevel: rendererSystem.foveationLevel
            };

            return vrDisplay.requestPresent([{
              source: this.canvas,
              attributes: presentationAttributes
            }]).then(enterVRSuccess, enterVRFailure);
          }
        }

        // No VR.
        enterVRSuccess();
        return Promise.resolve();

        // Callback that happens on enter VR success or enter fullscreen (any API).
        function enterVRSuccess (resolve) {
          // vrdisplaypresentchange fires only once when the first requestPresent is completed;
          // the first requestPresent could be called from ondisplayactivate and there is no way
          // to setup everything from there. Thus, we need to emulate another vrdisplaypresentchange
          // for the actual requestPresent. Need to make sure there are no issues with firing the
          // vrdisplaypresentchange multiple times.
          var event;
          if (window.hasNativeWebVRImplementation && !window.hasNativeWebXRImplementation) {
            event = new CustomEvent('vrdisplaypresentchange', {detail: {display: utils.device.getVRDisplay()}});
            window.dispatchEvent(event);
          }

          self.addState('vr-mode');
          self.emit('enter-vr', {target: self});
          // Lock to landscape orientation on mobile.
          if (!isWebXRAvailable && self.isMobile && screen.orientation && screen.orientation.lock) {
            screen.orientation.lock('landscape');
          }
          self.addFullScreenStyles();

          // On mobile, the polyfill handles fullscreen.
          // TODO: 07/16 Chromium builds break when `requestFullscreen`ing on a canvas
          // that we are also `requestPresent`ing. Until then, don't fullscreen if headset
          // connected.
          if (!self.isMobile && !self.checkHeadsetConnected()) {
            requestFullscreen(self.canvas);
          }

          self.renderer.setAnimationLoop(self.render);
          self.resize();
          if (resolve) { resolve(); }
        }

        function enterVRFailure (err) {
          if (err && err.message) {
            throw new Error('Failed to enter VR mode (`requestPresent`): ' + err.message);
          } else {
            throw new Error('Failed to enter VR mode (`requestPresent`).');
          }
        }
      },
      writable: true
    },
     /**
     * Call `exitPresent` if WebVR / WebXR or WebVR polyfill.
     * Handle events, states, fullscreen styles.
     *
     * @returns {Promise}
     */
    exitVR: {
      value: function () {
        var self = this;
        var vrDisplay;
        var vrManager = this.renderer.xr;

        // Don't exit VR if not in VR.
        if (!this.is('vr-mode')) { return Promise.resolve('Not in VR.'); }

        // Handle exiting VR if not yet already and in a headset or polyfill.
        if (this.checkHeadsetConnected() || this.isMobile) {
          vrManager.enabled = false;
          vrDisplay = utils.device.getVRDisplay();
          if (this.hasWebXR) {
            this.xrSession.removeEventListener('end', this.exitVRBound);
            // Capture promise to avoid errors.
            this.xrSession.end().then(function () {}, function () {});
            this.xrSession = undefined;
            vrManager.setSession(null);
          } else {
            if (vrDisplay.isPresenting) {
              return vrDisplay.exitPresent().then(exitVRSuccess, exitVRFailure);
            }
          }
        } else {
          exitFullscreen();
        }

        // Handle exiting VR in all other cases (2D fullscreen, external exit VR event).
        exitVRSuccess();

        return Promise.resolve();

        function exitVRSuccess () {
          self.removeState('vr-mode');
          self.removeState('ar-mode');
          // Lock to landscape orientation on mobile.
          if (self.isMobile && screen.orientation && screen.orientation.unlock) {
            screen.orientation.unlock();
          }
          // Exiting VR in embedded mode, no longer need fullscreen styles.
          if (self.hasAttribute('embedded')) { self.removeFullScreenStyles(); }

          self.resize();
          if (self.isIOS) { utils.forceCanvasResizeSafariMobile(self.canvas); }
          self.renderer.setPixelRatio(window.devicePixelRatio);
          self.emit('exit-vr', {target: self});
        }

        function exitVRFailure (err) {
          if (err && err.message) {
            throw new Error('Failed to exit VR mode (`exitPresent`): ' + err.message);
          } else {
            throw new Error('Failed to exit VR mode (`exitPresent`).');
          }
        }
      },
      writable: true
    },

    pointerRestricted: {
      value: function () {
        if (this.canvas) {
          var pointerLockElement = this.getPointerLockElement();
          if (pointerLockElement && pointerLockElement !== this.canvas && document.exitPointerLock) {
            // Recreate pointer lock on the canvas, if taken on another element.
            document.exitPointerLock();
          }

          if (this.canvas.requestPointerLock) {
            this.canvas.requestPointerLock();
          }
        }
      }
    },

    pointerUnrestricted: {
      value: function () {
        var pointerLockElement = this.getPointerLockElement();
        if (pointerLockElement && pointerLockElement === this.canvas && document.exitPointerLock) {
          document.exitPointerLock();
        }
      }
    },

    /**
     * Handle `vrdisplaypresentchange` event for exiting VR through other means than
     * `<ESC>` key. For example, GearVR back button on Oculus Browser.
     */
    onVRPresentChange: {
      value: function (evt) {
        // Polyfill places display inside the detail property
        var display = evt.display || evt.detail.display;
        // Entering VR.
        if (display && display.isPresenting) {
          this.enterVR();
          return;
        }
        // Exiting VR.
        this.exitVR();
      }
    },

    /**
     * Wraps Entity.getAttribute to take into account for systems.
     * If system exists, then return system data rather than possible component data.
     */
    getAttribute: {
      value: function (attr) {
        var system = this.systems[attr];
        if (system) { return system.data; }
        return AEntity.prototype.getAttribute.call(this, attr);
      }
    },

    /**
     * `getAttribute` used to be `getDOMAttribute` and `getComputedAttribute` used to be
     * what `getAttribute` is now. Now legacy code.
     */
    getComputedAttribute: {
      value: function (attr) {
        warn('`getComputedAttribute` is deprecated. Use `getAttribute` instead.');
        this.getAttribute(attr);
      }
    },

    /**
     * Wraps Entity.getDOMAttribute to take into account for systems.
     * If system exists, then return system data rather than possible component data.
     */
    getDOMAttribute: {
      value: function (attr) {
        var system = this.systems[attr];
        if (system) { return system.data; }
        return AEntity.prototype.getDOMAttribute.call(this, attr);
      }
    },

    /**
     * Wrap Entity.setAttribute to take into account for systems.
     * If system exists, then skip component initialization checks and do a normal
     * setAttribute.
     */
    setAttribute: {
      value: function (attr, value, componentPropValue) {
        var system = this.systems[attr];
        if (system) {
          ANode.prototype.setAttribute.call(this, attr, value);
          system.updateProperties(value);
          return;
        }
        AEntity.prototype.setAttribute.call(this, attr, value, componentPropValue);
      }
    },

    /**
     * @param {object} behavior - A component.
     */
    removeBehavior: {
      value: function (behavior) {
        var behaviorArr;
        var behaviorType;
        var behaviors = this.behaviors;
        var index;

        // Check if behavior has tick and/or tock and remove the behavior from the appropriate
        // array.
        for (behaviorType in behaviors) {
          if (!behavior[behaviorType]) { continue; }
          behaviorArr = this.behaviors[behaviorType];
          index = behaviorArr.indexOf(behavior);
          if (index !== -1) { behaviorArr.splice(index, 1); }
        }
      }
    },

    resize: {
      value: function () {
        var camera = this.camera;
        var canvas = this.canvas;
        var embedded;
        var isVRPresenting;
        var size;
        var isPresenting = this.renderer.xr.isPresenting;
        isVRPresenting = this.renderer.xr.enabled && isPresenting;

        // Do not update renderer, if a camera or a canvas have not been injected.
        // In VR mode, three handles canvas resize based on the dimensions returned by
        // the getEyeParameters function of the WebVR API. These dimensions are independent of
        // the window size, therefore should not be overwritten with the window's width and
        // height, // except when in fullscreen mode.
        if (!camera || !canvas || (this.is('vr-mode') && (this.isMobile || isVRPresenting))) {
          return;
        }

        // Update camera.
        embedded = this.getAttribute('embedded') && !this.is('vr-mode');
        size = getCanvasSize(canvas, embedded, this.maxCanvasSize, this.is('vr-mode'));
        camera.aspect = size.width / size.height;
        camera.updateProjectionMatrix();

        // Notify renderer of size change.
        this.renderer.setSize(size.width, size.height, false);
        this.emit('rendererresize', null, false);
      },
      writable: true
    },

    setupRenderer: {
      value: function () {
        var self = this;
        var renderer;
        var rendererAttr;
        var rendererAttrString;
        var rendererConfig;

        rendererConfig = {
          alpha: true,
          antialias: !isMobile,
          canvas: this.canvas,
          logarithmicDepthBuffer: false,
          powerPreference: 'high-performance'
        };

        this.maxCanvasSize = {height: 1920, width: 1920};

        if (this.hasAttribute('renderer')) {
          rendererAttrString = this.getAttribute('renderer');
          rendererAttr = utils.styleParser.parse(rendererAttrString);

          if (rendererAttr.precision) {
            rendererConfig.precision = rendererAttr.precision + 'p';
          }

          if (rendererAttr.antialias && rendererAttr.antialias !== 'auto') {
            rendererConfig.antialias = rendererAttr.antialias === 'true';
          }

          if (rendererAttr.logarithmicDepthBuffer && rendererAttr.logarithmicDepthBuffer !== 'auto') {
            rendererConfig.logarithmicDepthBuffer = rendererAttr.logarithmicDepthBuffer === 'true';
          }

          if (rendererAttr.alpha) {
            rendererConfig.alpha = rendererAttr.alpha === 'true';
          }

          this.maxCanvasSize = {
            width: rendererAttr.maxCanvasWidth
              ? parseInt(rendererAttr.maxCanvasWidth)
              : this.maxCanvasSize.width,
            height: rendererAttr.maxCanvasHeight
              ? parseInt(rendererAttr.maxCanvasHeight)
              : this.maxCanvasSize.height
          };
        }

        renderer = this.renderer = new THREE.WebGLRenderer(rendererConfig);
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.sortObjects = false;
        if (this.camera) { renderer.xr.setPoseTarget(this.camera.el.object3D); }
        this.addEventListener('camera-set-active', function () {
          renderer.xr.setPoseTarget(self.camera.el.object3D);
        });
        loadingScreen.setup(this, getCanvasSize);
      },
      writable: window.debug
    },

    /**
     * Handler attached to elements to help scene know when to kick off.
     * Scene waits for all entities to load.
     */
    play: {
      value: function () {
        var self = this;
        var sceneEl = this;

        if (this.renderStarted) {
          AEntity.prototype.play.call(this);
          return;
        }

        this.addEventListener('loaded', function () {
          var renderer = this.renderer;
          var vrDisplay;
          var vrManager = this.renderer.xr;
          AEntity.prototype.play.call(this);  // .play() *before* render.

          // WebXR Immersive navigation handler.
          if (this.hasWebXR && navigator.xr && navigator.xr.addEventListener) {
            navigator.xr.addEventListener('sessiongranted', function () { sceneEl.enterVR(); });
          }

          if (sceneEl.renderStarted) { return; }
          sceneEl.resize();

          // Kick off render loop.
          if (sceneEl.renderer) {
            if (window.performance) { window.performance.mark('render-started'); }
            loadingScreen.remove();
            vrDisplay = utils.device.getVRDisplay();
            if (vrDisplay && vrDisplay.isPresenting) {
              vrManager.setDevice(vrDisplay);
              vrManager.enabled = true;
              sceneEl.enterVR();
            }
            renderer.setAnimationLoop(this.render);
            sceneEl.renderStarted = true;
            sceneEl.emit('renderstart');
          }
        });

        // setTimeout to wait for all nodes to attach and run their callbacks.
        setTimeout(function () {
          AEntity.prototype.load.call(self);
        });
      }
    },

    /**
     * Wrap `updateComponent` to not initialize the component if the component has a system
     * (aframevr/aframe#2365).
     */
    updateComponent: {
      value: function (componentName) {
        if (componentName in systems) { return; }
        AEntity.prototype.updateComponent.apply(this, arguments);
      }
    },

    /**
     * Behavior-updater meant to be called from scene render.
     * Abstracted to a different function to facilitate unit testing (`scene.tick()`) without
     * needing to render.
     */
    tick: {
      value: function (time, timeDelta) {
        var i;
        var systems = this.systems;

        // Components.
        for (i = 0; i < this.behaviors.tick.length; i++) {
          if (!this.behaviors.tick[i].el.isPlaying) { continue; }
          this.behaviors.tick[i].tick(time, timeDelta);
        }

        // Systems.
        for (i = 0; i < this.systemNames.length; i++) {
          if (!systems[this.systemNames[i]].tick) { continue; }
          systems[this.systemNames[i]].tick(time, timeDelta);
        }
      }
    },

    /**
     * Behavior-updater meant to be called after scene render for post processing purposes.
     * Abstracted to a different function to facilitate unit testing (`scene.tock()`) without
     * needing to render.
     */
    tock: {
      value: function (time, timeDelta, camera) {
        var i;
        var systems = this.systems;

        // Components.
        for (i = 0; i < this.behaviors.tock.length; i++) {
          if (!this.behaviors.tock[i].el.isPlaying) { continue; }
          this.behaviors.tock[i].tock(time, timeDelta, camera);
        }

        // Systems.
        for (i = 0; i < this.systemNames.length; i++) {
          if (!systems[this.systemNames[i]].tock) { continue; }
          systems[this.systemNames[i]].tock(time, timeDelta, camera);
        }
      }
    },

    /**
     * The render loop.
     *
     * Updates animations.
     * Updates behaviors.
     * Renders with request animation frame.
     */
    render: {
      value: function (time, frame) {
        var renderer = this.renderer;

        this.frame = frame;
        this.delta = this.clock.getDelta() * 1000;
        this.time = this.clock.elapsedTime * 1000;

        if (this.isPlaying) { this.tick(this.time, this.delta); }
        var savedBackground = null;
        if (this.is('ar-mode')) {
          // In AR mode, don't render the default background. Hide it, then
          // restore it again after rendering.
          savedBackground = this.object3D.background;
          this.object3D.background = null;
        }
        renderer.render(this.object3D, this.camera);
        if (savedBackground) {
          this.object3D.background = savedBackground;
        }
      },
      writable: true
    }
  })
});

/**
 * Return the canvas size where the scene will be rendered.
 * Will be always the window size except when the scene is embedded.
 * The parent size (less than max size) will be returned in that case.
 *
 * @param {object} canvasEl - the canvas element
 * @param {boolean} embedded - Is the scene embedded?
 * @param {object} max - Max size parameters
 * @param {boolean} isVR - If in VR
 */
function getCanvasSize (canvasEl, embedded, maxSize, isVR) {
  if (embedded) {
    return {
      height: canvasEl.parentElement.offsetHeight,
      width: canvasEl.parentElement.offsetWidth
    };
  }
  return getMaxSize(maxSize, isVR);
}

/**
 * Return the canvas size. Will be the window size unless that size is greater than the
 * maximum size (1920x1920 by default).  The constrained size will be returned in that case,
 * maintaining aspect ratio
 *
 * @param {object} maxSize - Max size parameters (width and height).
 * @param {boolean} isVR - If in VR.
 * @returns {object} Width and height.
 */
function getMaxSize (maxSize, isVR) {
  var aspectRatio;
  var size;
  var pixelRatio = window.devicePixelRatio;

  size = {height: document.body.offsetHeight, width: document.body.offsetWidth};
  if (!maxSize || isVR || (maxSize.width === -1 && maxSize.height === -1)) {
    return size;
  }

  if (size.width * pixelRatio < maxSize.width &&
    size.height * pixelRatio < maxSize.height) {
    return size;
  }

  aspectRatio = size.width / size.height;

  if ((size.width * pixelRatio) > maxSize.width && maxSize.width !== -1) {
    size.width = Math.round(maxSize.width / pixelRatio);
    size.height = Math.round(maxSize.width / aspectRatio / pixelRatio);
  }

  if ((size.height * pixelRatio) > maxSize.height && maxSize.height !== -1) {
    size.height = Math.round(maxSize.height / pixelRatio);
    size.width = Math.round(maxSize.height * aspectRatio / pixelRatio);
  }

  return size;
}

function requestFullscreen (canvas) {
  var requestFullscreen =
    canvas.requestFullscreen ||
    canvas.webkitRequestFullscreen ||
    canvas.mozRequestFullScreen ||  // The capitalized `S` is not a typo.
    canvas.msRequestFullscreen;
  // Hide navigation buttons on Android.
  requestFullscreen.apply(canvas, [{navigationUI: 'hide'}]);
}

function exitFullscreen () {
  var fullscreenEl =
    document.fullscreenElement ||
    document.webkitFullscreenElement ||
    document.mozFullScreenElement;
  if (!fullscreenEl) { return; }
  if (document.exitFullscreen) {
    document.exitFullscreen();
  } else if (document.mozCancelFullScreen) {
    document.mozCancelFullScreen();
  } else if (document.webkitExitFullscreen) {
    document.webkitExitFullscreen();
  }
}

function setupCanvas (sceneEl) {
  var canvasEl;

  canvasEl = document.createElement('canvas');
  canvasEl.classList.add('a-canvas');
  // Mark canvas as provided/injected by A-Frame.
  canvasEl.dataset.aframeCanvas = true;
  sceneEl.appendChild(canvasEl);

  document.addEventListener('fullscreenchange', onFullScreenChange);
  document.addEventListener('mozfullscreenchange', onFullScreenChange);
  document.addEventListener('webkitfullscreenchange', onFullScreenChange);
  document.addEventListener('MSFullscreenChange', onFullScreenChange);

  // Prevent overscroll on mobile.
  canvasEl.addEventListener('touchmove', function (event) { event.preventDefault(); });

  // Set canvas on scene.
  sceneEl.canvas = canvasEl;
  sceneEl.emit('render-target-loaded', {target: canvasEl});
  // For unknown reasons a synchronous resize does not work on desktop when
  // entering/exiting fullscreen.
  setTimeout(bind(sceneEl.resize, sceneEl), 0);

  function onFullScreenChange () {
    var fullscreenEl =
      document.fullscreenElement ||
      document.mozFullScreenElement ||
      document.webkitFullscreenElement;
    // No fullscren element === exit fullscreen
    if (!fullscreenEl) { sceneEl.exitVR(); }
    document.activeElement.blur();
    document.body.focus();
  }
}
module.exports.setupCanvas = setupCanvas;  // For testing.