Newer
Older
reroad-test / 2020-ryusei / aframe-master / examples / showcase / link-traversal / js / components / link-controls.js
@ryusei ryusei on 22 Oct 2020 12 KB パノラマ表示
/* global AFRAME, THREE */

AFRAME.registerSystem('link-controls', {
  init: function () {
    this.peeking = false;
  }
});

/**
 * Link controls component. Provides the interaction model for links and hand controllers
 * When the user points to the link she can trigger the peek mode on the link to get a 360
 * preview of the linked experience without traversing to it.
 *
 * @member {object} hiddenEls - Stores the hidden elements during peek mode.
 */
AFRAME.registerComponent('link-controls', {
  schema: {hand: {default: 'left'}},

  init: function () {
    var el = this.el;
    var self = this;

    el.setAttribute('laser-controls', {hand: this.data.hand});
    el.setAttribute('raycaster', {far: 100, objects: '[link]'});
    // Wait for controller to connect before
    el.addEventListener('controllerconnected', function (evt) {
      var isMobile = AFRAME.utils.device.isMobile();
      var componentName = evt.detail.name;
      var gearVRorDaydream = componentName === 'daydream-controls' || componentName === 'gearvr-controls';
      // Hide second controller for one-controller platforms.
      if (gearVRorDaydream && self.data.hand === 'left') {
        self.el.setAttribute('raycaster', 'showLine', false);
        return;
      }
      self.controller = componentName;
      self.addControllerEventListeners();
      self.initTooltips();
      if (!isMobile) { self.initURLView(); }
    });
    this.cameraPosition = new THREE.Vector3();
    this.peeking = false;
    this.linkPositionRatio = 0.0;
    this.linkAnimationDuration = 250;
    this.bindMethods();
  },

  tick: function (time, delta) {
    this.animate(delta);
  },

  initURLView: function () {
    var urlEl = this.urlEl = document.createElement('a-entity');
    var urlBackgroundEl = this.urlBackgroundEl = document.createElement('a-entity');

    // Set text that displays the link title / url
    urlEl.setAttribute('text', {
      color: 'white',
      align: 'center',
      font: 'kelsonsans',
      value: '',
      width: 0.5
    });
    urlEl.setAttribute('position', '0 0.1 -0.25');
    urlEl.setAttribute('visible', false);
    urlBackgroundEl.setAttribute('position', '0 -0.0030 -0.001');
    urlBackgroundEl.setAttribute('slice9', 'width: 0.5; height: 0.1; left: 32; right: 32; top: 64; bottom: 32; src: images/tooltip.png');
    urlBackgroundEl.setAttribute('scale', '1 0.5 1');
    urlEl.appendChild(urlBackgroundEl);
    this.el.appendChild(urlEl);
  },

  tooltips: {
    'vive-controls': {
      left: {
        touchpad: {
          tooltip: 'text: Press and hold touchpad to peek link; width: 0.1; height: 0.04; targetPosition: 0 0.05 0',
          position: '0.1 0.05 0.048',
          rotation: '-90 0 0'
        },
        trigger: {
          tooltip: 'text: Press trigger to traverse link; width: 0.11; height: 0.04; targetPosition: 0 -0.06 0.06; lineHorizontalAlign: right;',
          position: '-0.11 -0.055 0.04',
          rotation: '-90 0 0'
        }
      },
      right: {
        touchpad: {
          tooltip: 'text: Press and hold touchpad to peek link; width: 0.1; height: 0.04; targetPosition: 0 0.05 0',
          position: '0.1 0.05 0.048',
          rotation: '-90 0 0'
        },
        trigger: {
          tooltip: 'text: Press trigger to traverse link; width: 0.11; height: 0.04; targetPosition: 0 -0.06 0.06; lineHorizontalAlign: right;',
          position: '-0.11 -0.055 0.04',
          rotation: '-90 0 0'
        }
      }
    },
    'oculus-touch-controls': {
      left: {
        xbutton: {
          tooltip: 'text: Press X to peek link; width: 0.1; height: 0.04; targetPosition: 0.01 0.05 0',
          position: '0.09 0.055 0.050',
          rotation: '-90 0 0'
        },
        trigger: {
          tooltip: 'text: Press trigger to traverse link; width: 0.11; height: 0.04; targetPosition: 0.01 -0.06 0.06; lineHorizontalAlign: right;',
          position: '-0.13 -0.055 0.04',
          rotation: '-90 0 0'
        }
      },
      right: {
        abutton: {
          tooltip: 'text: Press A to peek link; width: 0.1; height: 0.04; targetPosition: -0.01 0.05 0',
          position: '0.09 0.055 0.050',
          rotation: '-90 0 0'
        },
        trigger: {
          tooltip: 'text: Press trigger to traverse link; width: 0.11; height: 0.04; targetPosition: -0.005 -0.06 0.06; lineHorizontalAlign: right;',
          position: '-0.11 -0.055 0.04',
          rotation: '-90 0 0'
        }
      }
    },
    'daydream-controls': {
      touchpad: {
        tooltip: 'text: Touch to peek, click to traverse link; width: 0.11; height: 0.04; targetPosition: -0.005 -0.06 0.06; lineHorizontalAlign: right;',
        position: '-0.11 -0.055 0.04',
        rotation: '-90 0 0'
      }
    },
    'gearvr-controls': {
      touchpad: {
        tooltip: 'text: Touch to peek, click to traverse link; width: 0.11; height: 0.04; targetPosition: -0.005 -0.06 0.06; lineHorizontalAlign: right;',
        position: '-0.11 -0.055 0.04',
        rotation: '-90 0 0'
      }
    }
  },

  initTooltips: function () {
    var controllerTooltips;
    var tooltips = this.tooltips;
    var el = this.el;
    if (!this.controller) { return; }
    var hand = el.getAttribute(this.controller).hand;
    controllerTooltips = hand ? tooltips[this.controller][hand] : tooltips[this.controller];
    Object.keys(controllerTooltips).forEach(function (key) {
      var tooltip = controllerTooltips[key];
      var tooltipEl = document.createElement('a-entity');
      tooltipEl.setAttribute('tooltip', tooltip.tooltip);
      tooltipEl.setAttribute('position', tooltip.position);
      tooltipEl.setAttribute('rotation', tooltip.rotation);
      el.appendChild(tooltipEl);
    });
  },

  bindMethods: function () {
    this.onMouseEnter = this.onMouseEnter.bind(this);
    this.onMouseLeave = this.onMouseLeave.bind(this);
    this.startPeeking = this.startPeeking.bind(this);
    this.stopPeeking = this.stopPeeking.bind(this);
  },

  play: function () {
    var sceneEl = this.el.sceneEl;
    sceneEl.addEventListener('mouseenter', this.onMouseEnter);
    sceneEl.addEventListener('mouseleave', this.onMouseLeave);
    this.addControllerEventListeners();
  },

  pause: function () {
    var sceneEl = this.el.sceneEl;
    sceneEl.removeEventListener('mouseenter', this.onMouseEnter);
    sceneEl.removeEventListener('mouseleave', this.onMouseLeave);
    this.removeControllerEventListeners();
  },

  addControllerEventListeners: function () {
    var el = this.el;
    if (!this.controller) { return; }
    switch (this.controller) {
      case 'vive-controls':
        el.addEventListener('trackpaddown', this.startPeeking);
        el.addEventListener('trackpadup', this.stopPeeking);
        break;
      case 'daydream-controls':
        el.addEventListener('trackpadtouchstart', this.startPeeking);
        el.addEventListener('trackpadtouchend', this.stopPeeking);
        break;
      case 'oculus-touch-controls':
        el.addEventListener('xbuttondown', this.startPeeking);
        el.addEventListener('xbuttonup', this.stopPeeking);
        el.addEventListener('abuttondown', this.startPeeking);
        el.addEventListener('abuttonup', this.stopPeeking);
        break;
      case 'gearvr-controls':
        el.addEventListener('trackpadtouchstart', this.startPeeking);
        el.addEventListener('trackpadtouchend', this.stopPeeking);
        break;
      default:
        console.warn('Unknown controller ' + this.controller + '. Cannot attach link event listeners.');
    }
  },

  removeControllerEventListeners: function () {
    var el = this.el;
    switch (!this.controller) {
      case 'vive-controls':
        el.removeEventListeners('trackpaddown', this.startPeeking);
        el.removeEventListeners('trackpadup', this.stopPeeking);
        break;
      case 'daydream-controls':
        el.removeEventListeners('trackpadtouchstart', this.startPeeking);
        el.removeEventListeners('trackpadtouchend', this.stopPeeking);
        break;
      case 'oculus-touch-controls':
        el.removeEventListener('xbuttondown', this.startPeeking);
        el.removeEventListener('xbuttonup', this.stopPeeking);
        el.removeEventListener('abuttondown', this.startPeeking);
        el.removeEventListener('abuttonup', this.stopPeeking);
        break;
      case 'gearvr-controls':
        el.removeEventListener('trackpadtouchstart', this.startPeeking);
        el.removeEventListener('trackpadtouchend', this.stopPeeking);
        break;
      default:
        console.warn('Unknown controller ' + this.controller + '. Cannot remove link event listeners.');
    }
  },

  startPeeking: function () {
    var selectedLinkEl = this.selectedLinkEl;
    if (!selectedLinkEl || this.system.peeking || this.animatedEl) { return; }
    this.peeking = true;
    this.system.peeking = true;
    this.animatedEl = selectedLinkEl;
    this.animatedElInitPosition = selectedLinkEl.getAttribute('position');
    this.updateCameraPosition();
  },

  stopPeeking: function () {
    this.peeking = false;
    this.system.peeking = false;
  },

  updateCameraPosition: function () {
    var camera = this.el.sceneEl.camera;
    camera.parent.updateMatrixWorld();
    camera.updateMatrixWorld();
    this.cameraPosition.setFromMatrixPosition(camera.matrixWorld);
  },

  animate: (function () {
    var linkPosition = new THREE.Vector3();
    var portalToCameraVector = new THREE.Vector3();
    var easeOutCubic = function (t) { return (--t) * t * t + 1; };
    return function (delta) {
      var animatedEl = this.animatedEl;
      // There's no element to animate.
      if (!animatedEl) { return; }
      // User is not peeking and animation reached the end
      if (!this.peeking && this.linkPositionRatio === 0.0) {
        // Restore portal initial position after animation
        animatedEl.setAttribute('position', this.animatedElInitPosition);
        // Exit peekmode and show all the elements in the scene
        animatedEl.setAttribute('link', 'peekMode', false);
        animatedEl.components.link.showAll();
        // We're done with the animation
        this.animatedEl = undefined;
        return;
      }
      // If we're peeking and the animation towards the camera has finished
      if (this.peeking && this.linkPositionRatio === 1.0) {
        animatedEl.setAttribute('link', 'peekMode', true);
      } else {
        // Hide all elements during animation to avoid clipping artifacts
        animatedEl.components.link.hideAll();
      }
      // Calculate animation step
      var step = delta / this.linkAnimationDuration;
      // From [0..1] progress of the animation
      this.linkPositionRatio += this.peeking ? step : -step;
      // clamp to [0,1]
      this.linkPositionRatio = Math.min(Math.max(0.0, this.linkPositionRatio), 1.0);
      // Update Portal Position
      linkPosition.copy(this.animatedElInitPosition);
      // Move link towards the camera: Camera <----- Link
      portalToCameraVector.copy(this.cameraPosition).sub(linkPosition);
      var distanceToCamera = portalToCameraVector.length();
      // Unit vector
      portalToCameraVector.normalize();
      portalToCameraVector.multiplyScalar(distanceToCamera * easeOutCubic(this.linkPositionRatio));
      // Adds direction vector to the
      linkPosition.add(portalToCameraVector);
      // Hides / Shows link URL to prevent jarring animation when moving
      // the portal towards the user
      if (this.linkPositionRatio > 0.0 && this.linkPositionRatio < 1.0) {
        animatedEl.components.link.textEl.setAttribute('visible', false);
      } else {
        animatedEl.components.link.textEl.setAttribute('visible', true);
      }
      // We won't move the portal closer than 0.5m from the user.
      if (distanceToCamera <= 0.5 && this.peeking) { return; }
      // Update portal position
      animatedEl.setAttribute('position', linkPosition);
    };
  })(),

  onMouseEnter: function (evt) {
    var link;
    var previousSelectedLinkEl = this.selectedLinkEl;
    var selectedLinkEl = evt.detail.intersectedEl;
    var urlEl = this.urlEl;
    if (!selectedLinkEl || previousSelectedLinkEl || selectedLinkEl.components.link === undefined) { return; }
    selectedLinkEl.setAttribute('link', 'highlighted', true);
    this.selectedLinkElPosition = selectedLinkEl.getAttribute('position');
    this.selectedLinkEl = selectedLinkEl;
    if (!urlEl) { return; }
    link = selectedLinkEl.getAttribute('link');
    urlEl.setAttribute('text', 'value', link.title || link.href);
    urlEl.setAttribute('visible', true);
  },

  onMouseLeave: function (evt) {
    var selectedLinkEl = this.selectedLinkEl;
    var urlEl = this.urlEl;
    if (!selectedLinkEl || !evt.detail.intersectedEl) { return; }
    selectedLinkEl.setAttribute('link', 'highlighted', false);
    this.selectedLinkEl = undefined;
    if (!urlEl) { return; }
    urlEl.setAttribute('visible', false);
  }
});