Newer
Older
reroad-test / 2020-ryusei / aframe-master / src / components / link.js
@ryusei ryusei on 22 Oct 2020 12 KB パノラマ表示
var registerComponent = require('../core/component').registerComponent;
var registerShader = require('../core/shader').registerShader;
var THREE = require('../lib/three');

/**
 * Link component. Connect experiences and traverse between them in VR
 *
 * @member {object} hiddenEls - Store the hidden elements during peek mode.
 */
module.exports.Component = registerComponent('link', {
  schema: {
    backgroundColor: {default: 'red', type: 'color'},
    borderColor: {default: 'white', type: 'color'},
    highlighted: {default: false},
    highlightedColor: {default: '#24CAFF', type: 'color'},
    href: {default: ''},
    image: {type: 'asset'},
    on: {default: 'click'},
    peekMode: {default: false},
    title: {default: ''},
    titleColor: {default: 'white', type: 'color'},
    visualAspectEnabled: {default: false}
  },

  init: function () {
    this.navigate = this.navigate.bind(this);
    this.previousQuaternion = undefined;
    this.quaternionClone = new THREE.Quaternion();
    // Store hidden elements during peek mode so we can show them again later.
    this.hiddenEls = [];
  },

  update: function (oldData) {
    var data = this.data;
    var el = this.el;
    var backgroundColor;
    var strokeColor;

    if (!data.visualAspectEnabled) { return; }

    this.initVisualAspect();

    backgroundColor = data.highlighted ? data.highlightedColor : data.backgroundColor;
    strokeColor = data.highlighted ? data.highlightedColor : data.borderColor;
    el.setAttribute('material', 'backgroundColor', backgroundColor);
    el.setAttribute('material', 'strokeColor', strokeColor);

    if (data.on !== oldData.on) { this.updateEventListener(); }

    if (oldData.peekMode !== undefined &&
        data.peekMode !== oldData.peekMode) { this.updatePeekMode(); }

    if (!data.image || oldData.image === data.image) { return; }

    el.setAttribute('material', 'pano',
                    typeof data.image === 'string' ? data.image : data.image.src);
  },

  /*
   * Toggle all elements and full 360 preview of the linked page.
   */
  updatePeekMode: function () {
    var el = this.el;
    var sphereEl = this.sphereEl;
    if (this.data.peekMode) {
      this.hideAll();
      el.getObject3D('mesh').visible = false;
      sphereEl.setAttribute('visible', true);
    } else {
      this.showAll();
      el.getObject3D('mesh').visible = true;
      sphereEl.setAttribute('visible', false);
    }
  },

  play: function () {
    this.updateEventListener();
  },

  pause: function () {
    this.removeEventListener();
  },

  updateEventListener: function () {
    var el = this.el;
    if (!el.isPlaying) { return; }
    this.removeEventListener();
    el.addEventListener(this.data.on, this.navigate);
  },

  removeEventListener: function () {
    var on = this.data.on;
    if (!on) { return; }
    this.el.removeEventListener(on, this.navigate);
  },

  initVisualAspect: function () {
    var el = this.el;
    var semiSphereEl;
    var sphereEl;
    var textEl;

    if (!this.data.visualAspectEnabled || this.visualAspectInitialized) { return; }

    textEl = this.textEl = this.textEl || document.createElement('a-entity');
    sphereEl = this.sphereEl = this.sphereEl || document.createElement('a-entity');
    semiSphereEl = this.semiSphereEl = this.semiSphereEl || document.createElement('a-entity');

    // Set portal.
    el.setAttribute('geometry', {primitive: 'circle', radius: 1.0, segments: 64});
    el.setAttribute('material', {shader: 'portal', pano: this.data.image, side: 'double'});

    // Set text that displays the link title and URL.
    textEl.setAttribute('text', {
      color: this.data.titleColor,
      align: 'center',
      font: 'kelsonsans',
      value: this.data.title || this.data.href,
      width: 4
    });
    textEl.setAttribute('position', '0 1.5 0');
    el.appendChild(textEl);

    // Set sphere rendered when camera is close to portal to allow user to peek inside.
    semiSphereEl.setAttribute('geometry', {
      primitive: 'sphere',
      radius: 1.0,
      phiStart: 0,
      segmentsWidth: 64,
      segmentsHeight: 64,
      phiLength: 180,
      thetaStart: 0,
      thetaLength: 360
    });
    semiSphereEl.setAttribute('material', {
      shader: 'portal',
      borderEnabled: 0.0,
      pano: this.data.image,
      side: 'back'
    });
    semiSphereEl.setAttribute('rotation', '0 180 0');
    semiSphereEl.setAttribute('position', '0 0 0');
    semiSphereEl.setAttribute('visible', false);
    el.appendChild(semiSphereEl);

    // Set sphere rendered when camera is close to portal to allow user to peek inside.
    sphereEl.setAttribute('geometry', {
      primitive: 'sphere',
      radius: 10,
      segmentsWidth: 64,
      segmentsHeight: 64
    });
    sphereEl.setAttribute('material', {
      shader: 'portal',
      borderEnabled: 0.0,
      pano: this.data.image,
      side: 'back'
    });
    sphereEl.setAttribute('visible', false);
    el.appendChild(sphereEl);

    this.visualAspectInitialized = true;
  },

  navigate: function () {
    window.location = this.data.href;
  },

  /**
   * 1. Swap plane that represents portal with sphere with a hole when the camera is close
   * so user can peek inside portal. Sphere is rendered on oposite side of portal
   * from where user enters.
   * 2. Place the url/title above or inside portal depending on distance to camera.
   * 3. Face portal to camera when far away from user.
   */
  tick: (function () {
    var cameraWorldPosition = new THREE.Vector3();
    var elWorldPosition = new THREE.Vector3();
    var quaternion = new THREE.Quaternion();
    var scale = new THREE.Vector3();

    return function () {
      var el = this.el;
      var object3D = el.object3D;
      var camera = el.sceneEl.camera;
      var cameraPortalOrientation;
      var distance;
      var textEl = this.textEl;

      if (!this.data.visualAspectEnabled) { return; }

      // Update matrices
      object3D.updateMatrixWorld();
      camera.parent.updateMatrixWorld();
      camera.updateMatrixWorld();

      object3D.matrix.decompose(elWorldPosition, quaternion, scale);
      elWorldPosition.setFromMatrixPosition(object3D.matrixWorld);
      cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld);
      distance = elWorldPosition.distanceTo(cameraWorldPosition);

      if (distance > 20) {
        // Store original orientation to be restored when the portal stops facing the camera.
        if (!this.previousQuaternion) {
          this.quaternionClone.copy(quaternion);
          this.previousQuaternion = this.quaternionClone;
        }
        // If the portal is far away from the user, face portal to camera.
        object3D.lookAt(cameraWorldPosition);
      } else {
        // When portal is close to the user/camera.
        cameraPortalOrientation = this.calculateCameraPortalOrientation();
        // If user gets very close to portal, replace with holed sphere they can peek in.
        if (distance < 0.5) {
          // Configure text size and sphere orientation depending side user approaches portal.
          if (this.semiSphereEl.getAttribute('visible') === true) { return; }
          textEl.setAttribute('text', 'width', 1.5);
          if (cameraPortalOrientation <= 0.0) {
            textEl.setAttribute('position', '0 0 0.75');
            textEl.setAttribute('rotation', '0 180 0');
            this.semiSphereEl.setAttribute('rotation', '0 0 0');
          } else {
            textEl.setAttribute('position', '0 0 -0.75');
            textEl.setAttribute('rotation', '0 0 0');
            this.semiSphereEl.setAttribute('rotation', '0 180 0');
          }
          el.getObject3D('mesh').visible = false;
          this.semiSphereEl.setAttribute('visible', true);
          this.peekCameraPortalOrientation = cameraPortalOrientation;
        } else {
          // Calculate wich side the camera is approaching the camera (back / front).
          // Adjust text orientation based on camera position.
          if (cameraPortalOrientation <= 0.0) {
            textEl.setAttribute('rotation', '0 180 0');
          } else {
            textEl.setAttribute('rotation', '0 0 0');
          }
          textEl.setAttribute('text', 'width', 5);
          textEl.setAttribute('position', '0 1.5 0');
          el.getObject3D('mesh').visible = true;
          this.semiSphereEl.setAttribute('visible', false);
          this.peekCameraPortalOrientation = undefined;
        }
        if (this.previousQuaternion) {
          object3D.quaternion.copy(this.previousQuaternion);
          this.previousQuaternion = undefined;
        }
      }
    };
  })(),

  hideAll: function () {
    var el = this.el;
    var hiddenEls = this.hiddenEls;
    var self = this;
    if (hiddenEls.length > 0) { return; }
    el.sceneEl.object3D.traverse(function (object) {
      if (object && object.el && object.el.hasAttribute('link-controls')) { return; }
      if (!object.el || object === el.sceneEl.object3D || object.el === el ||
          object.el === self.sphereEl || object.el === el.sceneEl.cameraEl ||
          object.el.getAttribute('visible') === false || object.el === self.textEl ||
          object.el === self.semiSphereEl) {
        return;
      }
      object.el.setAttribute('visible', false);
      hiddenEls.push(object.el);
    });
  },

  showAll: function () {
    this.hiddenEls.forEach(function (el) { el.setAttribute('visible', true); });
    this.hiddenEls = [];
  },

  /**
   * Calculate whether the camera faces the front or back face of the portal.
   * @returns {number} > 0 if camera faces front of portal, < 0 if it faces back of portal.
   */
  calculateCameraPortalOrientation: (function () {
    var mat4 = new THREE.Matrix4();
    var cameraPosition = new THREE.Vector3();
    var portalNormal = new THREE.Vector3(0, 0, 1);
    var portalPosition = new THREE.Vector3(0, 0, 0);

    return function () {
      var el = this.el;
      var camera = el.sceneEl.camera;

      // Reset tmp variables.
      cameraPosition.set(0, 0, 0);
      portalNormal.set(0, 0, 1);
      portalPosition.set(0, 0, 0);

      // Apply portal orientation to the normal.
      el.object3D.matrixWorld.extractRotation(mat4);
      portalNormal.applyMatrix4(mat4);

      // Calculate portal world position.
      el.object3D.updateMatrixWorld();
      el.object3D.localToWorld(portalPosition);

      // Calculate camera world position.
      camera.parent.parent.updateMatrixWorld();
      camera.parent.updateMatrixWorld();
      camera.updateMatrixWorld();
      camera.localToWorld(cameraPosition);

      // Calculate vector from portal to camera.
      // (portal) -------> (camera)
      cameraPosition.sub(portalPosition).normalize();
      portalNormal.normalize();

      // Side where camera approaches portal is given by sign of dot product of portal normal
      // and portal to camera vectors.
      return Math.sign(portalNormal.dot(cameraPosition));
    };
  })(),

  remove: function () {
    this.removeEventListener();
  }
});

/* eslint-disable */
registerShader('portal', {
  schema: {
    borderEnabled: {default: 1.0, type: 'int', is: 'uniform'},
    backgroundColor: {default: 'red', type: 'color', is: 'uniform'},
    pano: {type: 'map', is: 'uniform'},
    strokeColor: {default: 'white', type: 'color', is: 'uniform'}
  },

  vertexShader: [
    'vec3 portalPosition;',
    'varying vec3 vWorldPosition;',
    'varying float vDistanceToCenter;',
    'varying float vDistance;',
    'void main() {',
    'vDistanceToCenter = clamp(length(position - vec3(0.0, 0.0, 0.0)), 0.0, 1.0);',
    'portalPosition = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;',
    'vDistance = length(portalPosition - cameraPosition);',
    'vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;',
    'gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);',
    '}'
  ].join('\n'),

  fragmentShader: [
    '#define RECIPROCAL_PI2 0.15915494',
    'uniform sampler2D pano;',
    'uniform vec3 strokeColor;',
    'uniform vec3 backgroundColor;',
    'uniform float borderEnabled;',
    'varying float vDistanceToCenter;',
    'varying float vDistance;',
    'varying vec3 vWorldPosition;',
    'void main() {',
    'vec3 direction = normalize(vWorldPosition - cameraPosition);',
    'vec2 sampleUV;',
    'float borderThickness = clamp(exp(-vDistance / 50.0), 0.6, 0.95);',
    'sampleUV.y = saturate(direction.y * 0.5  + 0.5);',
    'sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2 + 0.5;',
    'if (vDistanceToCenter > borderThickness && borderEnabled == 1.0) {',
    'gl_FragColor = vec4(strokeColor, 1.0);',
    '} else {',
    'gl_FragColor = mix(texture2D(pano, sampleUV), vec4(backgroundColor, 1.0), clamp(pow((vDistance / 15.0), 2.0), 0.0, 1.0));',
    '}',
    '}'
  ].join('\n')
});
/* eslint-enable */