import { MODES, CONTROLS, EVENTS } from '../Constants';
import { OrbitControls } from '../lib/controls/OrbitControls';
import { DeviceOrientationControls } from '../lib/controls/DeviceOrientationControls';
import { CardboardEffect } from '../lib/effects/CardboardEffect';
import { StereoEffect } from '../lib/effects/StereoEffect';
import { Widget } from '../widget/Widget';
import { Reticle } from '../interface/Reticle';
import { Infospot } from '../infospot/Infospot';
import { DataImage } from '../DataImage';
import { Panorama } from '../panorama/Panorama';
import { VideoPanorama } from '../panorama/VideoPanorama';
import { PanoMoment } from '../panorama/PanoMoment';
import { isAndroid } from '../utils/Utility';
import * as THREE from 'three';
import TWEEN from '@tweenjs/tween.js';
/**
* @classdesc Viewer contains pre-defined scene, camera and renderer
* @constructor
* @param {object} [options] - Use custom or default config options
* @param {HTMLElement} [options.container] - A HTMLElement to host the canvas
* @param {THREE.Scene} [options.scene=THREE.Scene] - A THREE.Scene which contains panorama and 3D objects
* @param {THREE.Camera} [options.camera=THREE.PerspectiveCamera] - A THREE.Camera to view the scene
* @param {THREE.WebGLRenderer} [options.renderer=THREE.WebGLRenderer] - A THREE.WebGLRenderer to render canvas
* @param {boolean} [options.controlBar=true] - Show/hide control bar on the bottom of the container
* @param {array} [options.controlButtons=[]] - Button names to mount on controlBar if controlBar exists, Defaults to ['fullscreen', 'setting', 'video']
* @param {boolean} [options.autoHideControlBar=false] - Auto hide control bar when click on non-active area
* @param {boolean} [options.autoHideInfospot=true] - Auto hide infospots when click on non-active area
* @param {boolean} [options.horizontalView=false] - Allow only horizontal camera control
* @param {number} [options.clickTolerance=10] - Distance tolerance to tigger click / tap event
* @param {number} [options.cameraFov=60] - Camera field of view value
* @param {boolean} [options.enableReticle=false] - Enable reticle for mouseless interaction other than VR mode
* @param {number} [options.dwellTime=1500] - Dwell time for reticle selection in ms
* @param {boolean} [options.autoReticleSelect=true] - Auto select a clickable target after dwellTime
* @param {boolean} [options.viewIndicator=false] - Adds an angle view indicator in upper left corner
* @param {number} [options.indicatorSize=30] - Size of View Indicator
* @param {string} [options.output=null] - Whether and where to output raycast position. Could be 'console' or 'overlay'
* @param {boolean} [options.autoRotate=false] - Auto rotate
* @param {number} [options.autoRotateSpeed=2.0] - Auto rotate speed as in degree per second. Positive is counter-clockwise and negative is clockwise.
* @param {number} [options.autoRotateActivationDuration=5000] - Duration before auto rotatation when no user interactivity in ms
* @param {THREE.Vector3} [options.initialLookAt=new THREE.Vector3( 0, 0, -Number.MAX_SAFE_INTEGER )] - Initial looking at vector
* @param {boolean} [options.momentum=true] - Use momentum even during mouse/touch move
* @param {number} [options.rotateSpeed=-1.0] - Drag Rotation Speed
* @param {number} [options.dampingFactor=.9] - Damping factor
* @param {number} [options.speedLimit=Number.MAX_VALUE] - Speed limit for rotation, defaults to unlimited
*/
function Viewer ( options = {} ) {
this.options = Object.assign( {
container: this.setupContainer( options.container ),
controlBar: true,
controlButtons: [ 'fullscreen', 'setting', 'video' ],
autoHideControlBar: false,
autoHideInfospot: true,
horizontalView: false,
clickTolerance: 10,
cameraFov: 60,
reverseDragging: false,
enableReticle: false,
dwellTime: 1500,
autoReticleSelect: true,
viewIndicator: false,
indicatorSize: 30,
output: null,
autoRotate: false,
autoRotateSpeed: 2.0,
autoRotateActivationDuration: 5000,
initialLookAt: new THREE.Vector3( 0, 0, -Number.MAX_SAFE_INTEGER ),
momentum: true,
rotateSpeed: -1.0,
dampingFactor: 0.9,
speedLimit: Number.MAX_VALUE
}, options );
const { container, cameraFov, controlBar, controlButtons, viewIndicator, indicatorSize, enableReticle, reverseDragging, output, scene, camera, renderer } = this.options;
const { clientWidth, clientHeight } = container;
this.container = container;
this.scene = this.setupScene( scene );
this.sceneReticle = new THREE.Scene();
this.camera = this.setupCamera( cameraFov, clientWidth / clientHeight, camera );
this.renderer = this.setupRenderer( renderer, container );
this.reticle = this.addReticle( this.camera, this.sceneReticle );
this.control = this.setupControls( this.camera, container );
this.effect = this.setupEffects( this.renderer, container );
this.mode = MODES.NORMAL;
this.panorama = null;
this.widget = null;
this.hoverObject = null;
this.infospot = null;
this.pressEntityObject = null;
this.pressObject = null;
this.raycaster = new THREE.Raycaster();
this.raycasterPoint = new THREE.Vector2();
this.userMouse = new THREE.Vector2();
this.updateCallbacks = [];
this.requestAnimationId = null;
this.cameraFrustum = new THREE.Frustum();
this.cameraViewProjectionMatrix = new THREE.Matrix4();
this.autoRotateRequestId = null;
this.outputDivElement = null;
this.touchSupported = 'ontouchstart' in window || window.DocumentTouch && document instanceof DocumentTouch;
this.tweenLeftAnimation = new TWEEN.Tween();
this.tweenUpAnimation = new TWEEN.Tween();
this.tweenCanvasOpacityOut = new TWEEN.Tween();
this.tweenCanvasOpacityIn = new TWEEN.Tween();
this.outputEnabled = false;
this.viewIndicatorSize = indicatorSize;
this.tempEnableReticle = enableReticle;
this.setupTween();
this.handlerMouseUp = this.onMouseUp.bind( this );
this.handlerMouseDown = this.onMouseDown.bind( this );
this.handlerMouseMove = this.onMouseMove.bind( this );
this.handlerWindowResize = this.onWindowResize.bind( this );
this.handlerKeyDown = this.onKeyDown.bind( this );
this.handlerKeyUp = this.onKeyUp.bind( this );
this.handlerTap = this.onTap.bind( this, { clientX: clientWidth / 2, clientY: clientHeight / 2 } );
if ( controlBar ) this.addDefaultControlBar( controlButtons );
if ( viewIndicator ) this.addViewIndicator();
if ( reverseDragging ) this.reverseDraggingDirection();
if ( enableReticle ) this.enableReticleControl(); else this.registerMouseAndTouchEvents();
if ( output === 'overlay' ) this.addOutputElement();
this.registerEventListeners();
this.animate.call( this );
};
Viewer.prototype = Object.assign( Object.create( THREE.EventDispatcher.prototype ), {
constructor: Viewer,
setupScene: function ( scene = new THREE.Scene() ) {
return scene;
},
setupCamera: function ( cameraFov, ratio, camera = new THREE.PerspectiveCamera( cameraFov, ratio, 1, 10000 ) ) {
camera.position.set( 0, 0, 1 );
return camera;
},
setupRenderer: function ( renderer = new THREE.WebGLRenderer( { alpha: true, antialias: false } ), container ) {
const { clientWidth, clientHeight } = container;
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( clientWidth, clientHeight );
renderer.setClearColor( 0x000000, 0 );
renderer.autoClear = false;
renderer.domElement.classList.add( 'panolens-canvas' );
renderer.domElement.style.display = 'block';
renderer.domElement.style.transition = 'opacity 0.5s ease';
container.style.backgroundColor = '#000';
container.appendChild( renderer.domElement );
return renderer;
},
setupControls: function ( camera, container ) {
const { autoRotate, autoRotateSpeed, momentum, rotateSpeed, dampingFactor, speedLimit, horizontalView } = this.options;
const orbit = Object.assign( new OrbitControls( camera, container ), {
id: 'orbit',
index: CONTROLS.ORBIT,
noPan: true,
minDistance: 1.0,
autoRotate,
autoRotateSpeed,
momentum,
rotateSpeed,
dampingFactor,
speedLimit
} );
if ( horizontalView ) {
orbit.minPolarAngle = Math.PI / 2;
orbit.maxPolarAngle = Math.PI / 2;
}
const orient = Object.assign( new DeviceOrientationControls( camera ), {
id: 'device-orientation',
index: CONTROLS.DEVICEORIENTATION,
enabled: false
} );
this.controls = [ orbit, orient ];
this.OrbitControls = orbit;
this.DeviceOrientationControls = orient;
return orbit;
},
setupEffects: function ( renderer, { clientWidth, clientHeight } ) {
const cardboard = new CardboardEffect( renderer );
cardboard.setSize( clientWidth, clientHeight );
const stereo = new StereoEffect( renderer );
stereo.setSize( clientWidth, clientHeight );
this.CardboardEffect = cardboard;
this.StereoEffect = stereo;
return cardboard;
},
setupContainer: function ( container ) {
if ( container ) {
container._width = container.clientWidth;
container._height = container.clientHeight;
return container;
} else {
const element = document.createElement( 'div' );
element.classList.add( EVENTS.CONTAINER );
element.style.width = '100%';
element.style.height = '100%';
document.body.appendChild( element );
return element;
}
},
setupTween: function() {
this.tweenCanvasOpacityOut.to({}, 500).easing(TWEEN.Easing.Exponential.Out);
this.tweenCanvasOpacityIn.to({}, 500).easing(TWEEN.Easing.Exponential.Out);
this.tweenCanvasOpacityOut.chain(this.tweenCanvasOpacityIn);
},
/**
* Add an object to the scene
* Automatically hookup with panolens-viewer-handler listener
* to communicate with viewer method
* @param {THREE.Object3D} object - The object to be added
* @memberOf Viewer
* @instance
*/
add: function ( object ) {
const { container, scene, camera, controls, options: { initialLookAt } } = this;
if ( arguments.length > 1 ) {
for ( let i = 0; i < arguments.length; i ++ ) {
this.add( arguments[ i ] );
}
return this;
}
scene.add( object );
// All object added to scene has EVENTS.VIEWER_HANDLER event to handle viewer communication
if ( object.addEventListener ) {
object.addEventListener( EVENTS.VIEWER_HANDLER, this.eventHandler.bind( this ) );
}
if ( object instanceof Panorama ) {
// Dispatch viewer variables to panorama
object.dispatchEvent( { type: EVENTS.CONTAINER, container } );
object.dispatchEvent( { type: 'panolens-scene', scene } );
object.dispatchEvent( { type: EVENTS.CAMERA, camera } );
object.dispatchEvent( { type: EVENTS.CONTROLS, controls } );
// Hookup default panorama event listeners
this.addPanoramaEventListener( object );
if ( !this.panorama ) {
this.setPanorama( object );
this.setControlCenter( initialLookAt );
}
}
},
/**
* Remove an object from the scene
* @param {THREE.Object3D} object - Object to be removed
* @memberOf Viewer
* @instance
*/
remove: function ( object ) {
if ( object.removeEventListener ) {
object.removeEventListener( EVENTS.VIEWER_HANDLER, this.eventHandler.bind( this ) );
}
this.scene.remove( object );
},
/**
* Add default control bar
* @param {array} array - The control buttons array
* @memberOf Viewer
* @instance
*/
addDefaultControlBar: function ( array ) {
if ( this.widget ) {
console.warn( 'Default control bar exists' );
return;
}
const widget = new Widget( this.container );
widget.addEventListener( EVENTS.VIEWER_HANDLER, this.eventHandler.bind( this ) );
widget.addControlBar();
array.forEach( buttonName => {
widget.addControlButton( buttonName );
} );
this.widget = widget;
},
/**
* Set a panorama to be the current one
* @param {Panorama} pano - Panorama to be set
* @memberOf Viewer
* @instance
*/
setPanorama: function ( ep ) {
const lp = this.panorama;
if ( ep instanceof Panorama && lp !== ep ) {
// Clear exisiting infospot
this.hideInfospot();
if( lp ) {
if( ep instanceof PanoMoment ) {
const onLeaveComplete = () => {
lp.removeEventListener( EVENTS.LEAVE_COMPLETE, onLeaveComplete );
delete lp._onLeaveComplete;
if ( ep.active && ep.loaded ) ep.fadeIn();
};
lp._onLeaveComplete = onLeaveComplete;
lp.addEventListener( EVENTS.LEAVE_COMPLETE, onLeaveComplete );
}
if ( lp._onReady ) {
lp.removeEventListener( EVENTS.READY, lp._onReady );
delete lp._onReady;
}
if ( lp._onEnterFadeStart ) {
lp.removeEventListener( EVENTS.ENTER_FADE_START, lp._onEnterFadeStart );
delete lp._onEnterFadeStart;
}
}
if( ep._onLeaveComplete ) {
ep.removeEventListener( EVENTS.LEAVE_COMPLETE, ep._onLeaveComplete );
delete ep._onLeaveComplete;
}
const onReady = () => {
ep.removeEventListener( EVENTS.READY, onReady );
delete ep._onReady;
if( !ep.active ) return;
if( ep instanceof PanoMoment ) {
if(!lp || (lp && !lp._onLeaveComplete)) ep.fadeIn();
} else {
ep.fadeIn();
}
};
const onEnterFadeStart = function () {
if ( lp ) { lp.onLeave(); }
ep.removeEventListener( EVENTS.ENTER_FADE_START, onEnterFadeStart );
delete ep._onEnterFadeStart;
};
ep.addEventListener( EVENTS.READY, onReady );
ep.addEventListener( EVENTS.ENTER_FADE_START, onEnterFadeStart );
ep._onReady = onReady;
ep._onEnterFadeStart = onEnterFadeStart;
this.panorama = ep;
requestAnimationFrame(() => ep.onEnter());
}
},
/**
* Event handler to execute commands from child objects
* @param {object} event - The dispatched event with method as function name and data as an argument
* @memberOf Viewer
* @instance
*/
eventHandler: function ( event ) {
if ( event.method && this[ event.method ] ) {
this[ event.method ]( event.data );
}
},
/**
* Dispatch event to all descendants
* @param {object} event - Event to be passed along
* @memberOf Viewer
* @instance
*/
dispatchEventToChildren: function ( event ) {
this.scene.traverse( function ( object ) {
if ( object.dispatchEvent ) {
object.dispatchEvent( event );
}
});
},
/**
* Set widget content
* @method activateWidgetItem
* @param {integer} controlIndex - Control index
* @param {integer} mode - Modes for effects
* @memberOf Viewer
* @instance
*/
activateWidgetItem: function ( controlIndex, mode ) {
const mainMenu = this.widget.mainMenu;
const ControlMenuItem = mainMenu.children[ 0 ];
const ModeMenuItem = mainMenu.children[ 1 ];
let item;
if ( controlIndex !== undefined ) {
switch ( controlIndex ) {
case 0:
item = ControlMenuItem.subMenu.children[ 1 ];
break;
case 1:
item = ControlMenuItem.subMenu.children[ 2 ];
break;
default:
item = ControlMenuItem.subMenu.children[ 1 ];
break;
}
ControlMenuItem.subMenu.setActiveItem( item );
ControlMenuItem.setSelectionTitle( item.textContent );
}
if ( mode !== undefined ) {
switch( mode ) {
case MODES.CARDBOARD:
item = ModeMenuItem.subMenu.children[ 2 ];
break;
case MODES.STEREO:
item = ModeMenuItem.subMenu.children[ 3 ];
break;
default:
item = ModeMenuItem.subMenu.children[ 1 ];
break;
}
ModeMenuItem.subMenu.setActiveItem( item );
ModeMenuItem.setSelectionTitle( item.textContent );
}
},
/**
* Enable rendering effect
* @param {MODES} mode - Modes for effects
* @memberOf Viewer
* @instance
*/
enableEffect: function ( mode ) {
if ( this.mode === mode ) { return; }
if ( mode === MODES.NORMAL ) { this.disableEffect(); return; }
else { this.mode = mode; }
const fov = this.camera.fov;
switch( mode ) {
case MODES.CARDBOARD:
this.effect = this.CardboardEffect;
this.enableReticleControl();
break;
case MODES.STEREO:
this.effect = this.StereoEffect;
this.enableReticleControl();
break;
default:
this.effect = null;
this.disableReticleControl();
break;
}
this.activateWidgetItem( undefined, this.mode );
/**
* Dual eye effect event
* @type {object}
* @event Infospot#panolens-dual-eye-effect
* @property {MODES} mode - Current display mode
*/
this.dispatchEventToChildren( { type: 'panolens-dual-eye-effect', mode: this.mode } );
// Force effect stereo camera to update by refreshing fov
this.camera.fov = fov + 10e-3;
this.effect.setSize( this.container.clientWidth, this.container.clientHeight );
this.render();
this.camera.fov = fov;
/**
* Dispatch mode change event
* @type {object}
* @event Viewer#mode-change
* @property {MODES} mode - Current display mode
*/
this.dispatchEvent( { type: EVENTS.MODE_CHANGE, mode: this.mode } );
},
/**
* Disable additional rendering effect
* @memberOf Viewer
* @instance
*/
disableEffect: function () {
if ( this.mode === MODES.NORMAL ) { return; }
this.mode = MODES.NORMAL;
this.disableReticleControl();
this.activateWidgetItem( undefined, this.mode );
/**
* Dual eye effect event
* @type {object}
* @event Infospot#panolens-dual-eye-effect
* @property {MODES} mode - Current display mode
*/
this.dispatchEventToChildren( { type: 'panolens-dual-eye-effect', mode: this.mode } );
this.renderer.setSize( this.container.clientWidth, this.container.clientHeight );
this.render();
/**
* Dispatch mode change event
* @type {object}
* @event Viewer#mode-change
* @property {MODES} mode - Current display mode
*/
this.dispatchEvent( { type: EVENTS.MODE_CHANGE, mode: this.mode } );
},
/**
* Enable reticle control
* @memberOf Viewer
* @instance
*/
enableReticleControl: function () {
if ( this.reticle.visible ) { return; }
this.tempEnableReticle = true;
// Register reticle event and unregister mouse event
this.unregisterMouseAndTouchEvents();
this.reticle.show();
this.registerReticleEvent();
this.updateReticleEvent();
},
/**
* Disable reticle control
* @memberOf Viewer
* @instance
*/
disableReticleControl: function () {
this.tempEnableReticle = false;
// Register mouse event and unregister reticle event
if ( !this.options.enableReticle ) {
this.reticle.hide();
this.unregisterReticleEvent();
this.registerMouseAndTouchEvents();
} else {
this.updateReticleEvent();
}
},
/**
* Enable auto rotation
* @memberOf Viewer
* @instance
*/
enableAutoRate: function () {
this.options.autoRotate = true;
this.OrbitControls.autoRotate = true;
},
/**
* Disable auto rotation
* @memberOf Viewer
* @instance
*/
disableAutoRate: function () {
clearTimeout( this.autoRotateRequestId );
this.options.autoRotate = false;
this.OrbitControls.autoRotate = false;
},
/**
* Toggle video play or stop
* @param {boolean} pause
* @memberOf Viewer
* @instance
* @fires Viewer#video-toggle
*/
toggleVideoPlay: function ( pause ) {
if ( this.panorama instanceof VideoPanorama ) {
/**
* Toggle video event
* @type {object}
* @event Viewer#video-toggle
*/
this.panorama.dispatchEvent( { type: 'video-toggle', pause: pause } );
}
},
/**
* Set currentTime in a video
* @param {number} percentage - Percentage of a video. Range from 0.0 to 1.0
* @memberOf Viewer
* @instance
* @fires Viewer#video-time
*/
setVideoCurrentTime: function ( percentage ) {
if ( this.panorama instanceof VideoPanorama ) {
/**
* Setting video time event
* @type {object}
* @event Viewer#video-time
* @property {number} percentage - Percentage of a video. Range from 0.0 to 1.0
*/
this.panorama.dispatchEvent( { type: 'video-time', percentage: percentage } );
}
},
/**
* This will be called when video updates if an widget is present
* @param {number} percentage - Percentage of a video. Range from 0.0 to 1.0
* @memberOf Viewer
* @instance
* @fires Viewer#video-update
*/
onVideoUpdate: function ( percentage ) {
const { widget } = this;
/**
* Video update event
* @type {object}
* @event Viewer#video-update
* @property {number} percentage - Percentage of a video. Range from 0.0 to 1.0
*/
if( widget ) { widget.dispatchEvent( { type: 'video-update', percentage: percentage } ); }
},
/**
* Add update callback to be called every animation frame
* @param {function} callback
* @memberOf Viewer
* @instance
*/
addUpdateCallback: function ( fn ) {
if ( fn ) {
this.updateCallbacks.push( fn );
}
},
/**
* Remove update callback
* @param {function} fn - The function to be removed
* @memberOf Viewer
* @instance
*/
removeUpdateCallback: function ( fn ) {
const index = this.updateCallbacks.indexOf( fn );
if ( fn && index >= 0 ) {
this.updateCallbacks.splice( index, 1 );
}
},
/**
* Show video widget
* @memberOf Viewer
* @instance
*/
showVideoWidget: function () {
const { widget } = this;
/**
* Show video widget event
* @type {object}
* @event Viewer#video-control-show
*/
if( widget ) { widget.dispatchEvent( { type: 'video-control-show' } ); }
},
/**
* Hide video widget
* @memberOf Viewer
* @instance
*/
hideVideoWidget: function () {
const { widget } = this;
/**
* Hide video widget
* @type {object}
* @event Viewer#video-control-hide
*/
if( widget ) { widget.dispatchEvent( { type: 'video-control-hide' } ); }
},
/**
* Update video play button
* @param {boolean} paused
* @memberOf Viewer
* @instance
*/
updateVideoPlayButton: function ( paused ) {
const { widget } = this;
if ( widget && widget.videoElement && widget.videoElement.controlButton ) {
widget.videoElement.controlButton.update( paused );
}
},
/**
* Add default panorama event listeners
* @param {Panorama} pano - The panorama to be added with event listener
* @memberOf Viewer
* @instance
*/
addPanoramaEventListener: function ( pano ) {
// Set camera control on every panorama
pano.addEventListener( EVENTS.ENTER, this.setCameraControl.bind( this ) );
// Show and hide widget event only when it's VideoPanorama
if ( pano instanceof VideoPanorama ) {
pano.addEventListener( EVENTS.ENTER_FADE_START, this.showVideoWidget.bind( this ) );
pano.addEventListener( EVENTS.LEAVE_START, function () {
if ( !(this.panorama instanceof VideoPanorama) ) {
this.hideVideoWidget.call( this );
}
}.bind( this ) );
}
},
/**
* Set camera control
* @memberOf Viewer
* @instance
*/
setCameraControl: function () {
if( this.panorama ) this.OrbitControls.target.copy( this.panorama.position );
},
/**
* Get current camera control
* @return {object} - Current navigation control
* @memberOf Viewer
* @instance
* @returns {THREE.OrbitControls|THREE.DeviceOrientationControls}
*/
getControl: function () {
return this.control;
},
/**
* Get scene
* @memberOf Viewer
* @instance
* @return {THREE.Scene} - Current scene which the viewer is built on
*/
getScene: function () {
return this.scene;
},
/**
* Get camera
* @memberOf Viewer
* @instance
* @return {THREE.Camera} - The scene camera
*/
getCamera: function () {
return this.camera;
},
/**
* Get renderer
* @memberOf Viewer
* @instance
* @return {THREE.WebGLRenderer} - The renderer using webgl
*/
getRenderer: function () {
return this.renderer;
},
/**
* Get container
* @memberOf Viewer
* @instance
* @return {HTMLElement} - The container holds rendererd canvas
*/
getContainer: function () {
return this.container;
},
/**
* Get control id
* @memberOf Viewer
* @instance
* @return {string} - Control id. 'orbit' or 'device-orientation'
*/
getControlId: function () {
return this.control.id;
},
/**
* Get next navigation control id
* @memberOf Viewer
* @instance
* @return {string} - Next control id
*/
getNextControlId: function () {
return this.controls[ this.getNextControlIndex() ].id;
},
/**
* Get next navigation control index
* @memberOf Viewer
* @instance
* @return {number} - Next control index
*/
getNextControlIndex: function () {
const controls = this.controls;
const control = this.control;
const nextIndex = controls.indexOf( control ) + 1;
return ( nextIndex >= controls.length ) ? 0 : nextIndex;
},
/**
* Set field of view of camera
* @param {number} fov
* @memberOf Viewer
* @instance
*/
setCameraFov: function ( fov ) {
this.camera.fov = fov;
this.camera.updateProjectionMatrix();
},
/**
* Get raycasted point of current panorama
* @memberof Viewer
* @instance
* @returns {THREE.Vector3}
*/
getRaycastViewCenter: function () {
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera( new THREE.Vector2( 0, 0 ), this.camera );
const intersect = raycaster.intersectObject( this.panorama );
return intersect.length > 0 ? intersect[ 0 ].point : new THREE.Vector3( 0, 0, -1 );
},
/**
* Enable control by index
* @param {CONTROLS} index - Index of camera control
* @memberOf Viewer
* @instance
*/
enableControl: function ( index = CONTROLS.ORBIT ) {
const { control: { index: currentControlIndex }, OrbitControls, DeviceOrientationControls, container } = this;
const canvas = container.querySelector('canvas');
if( index === currentControlIndex ) { // ignore
return;
} else if( index === CONTROLS.DEVICEORIENTATION ) { // device orientation
this.tweenCanvasOpacityOut.onStart(() => {
OrbitControls.enabled = false;
DeviceOrientationControls.enabled = false;
canvas.style.opacity = 0;
});
this.tweenCanvasOpacityIn.onStart(() => {
OrbitControls.enabled = true;
DeviceOrientationControls.connect();
canvas.style.opacity = 1;
});
this.tweenCanvasOpacityOut.start();
} else {
const { getAlpha, getBeta } = DeviceOrientationControls;
const alpha = -getAlpha();
const beta = Math.PI / 2 - getBeta();
const center = this.getRaycastViewCenter();
this.tweenCanvasOpacityOut.onStart(() => {
OrbitControls.enabled = false;
DeviceOrientationControls.disconnect();
canvas.style.opacity = 0;
});
this.tweenCanvasOpacityIn.onStart(function() {
OrbitControls.enabled = true;
this.rotateControlLeft(alpha);
this.rotateControlUp(beta);
this.setControlCenter(center);
canvas.style.opacity = 1;
}.bind(this));
this.tweenCanvasOpacityOut.start();
}
this.control = this.controls[ index ];
this.activateWidgetItem( index, undefined );
},
/**
* Disable current control
* @memberOf Viewer
* @instance
*/
disableControl: function () {
this.control.enabled = false;
},
/**
* Toggle next control
* @memberOf Viewer
* @instance
*/
toggleNextControl: function () {
this.enableControl( this.getNextControlIndex() );
},
/**
* Screen Space Projection
* @memberOf Viewer
* @instance
*/
getScreenVector: function ( worldVector ) {
const vector = worldVector.clone();
const widthHalf = ( this.container.clientWidth ) / 2;
const heightHalf = this.container.clientHeight / 2;
vector.project( this.camera );
vector.x = ( vector.x * widthHalf ) + widthHalf;
vector.y = - ( vector.y * heightHalf ) + heightHalf;
vector.z = 0;
return vector;
},
/**
* Check Sprite in Viewport
* @memberOf Viewer
* @instance
*/
checkSpriteInViewport: function ( sprite ) {
this.camera.matrixWorldInverse.getInverse( this.camera.matrixWorld );
this.cameraViewProjectionMatrix.multiplyMatrices( this.camera.projectionMatrix, this.camera.matrixWorldInverse );
this.cameraFrustum.setFromProjectionMatrix( this.cameraViewProjectionMatrix );
return sprite.visible && this.cameraFrustum.intersectsSprite( sprite );
},
/**
* Reverse dragging direction
* @memberOf Viewer
* @instance
*/
reverseDraggingDirection: function () {
console.warn('reverseDragging option is deprecated. Please use rotateSpeed to indicate strength and direction');
this.OrbitControls.rotateSpeed *= -1;
},
/**
* Add reticle
* @memberOf Viewer
* @instance
*/
addReticle: function ( camera, sceneReticle ) {
const reticle = new Reticle( 0xffffff, true, this.options.dwellTime );
reticle.hide();
camera.add( reticle );
sceneReticle.add( camera );
return reticle;
},
rotateControlLeft: function ( left ) {
this.OrbitControls.rotateLeftStatic( left );
},
rotateControlUp: function ( up ) {
this.OrbitControls.rotateUpStatic( up );
},
rotateOrbitControl: function ( left, up ) {
this.rotateControlLeft( left );
this.rotateControlUp( up );
},
calculateCameraDirectionDelta: function ( vector ) {
let ha, va, chv, cvv, hv, vv, vptc;
chv = this.camera.getWorldDirection( new THREE.Vector3() );
cvv = chv.clone();
vptc = this.panorama.getWorldPosition( new THREE.Vector3() ).sub( this.camera.getWorldPosition( new THREE.Vector3() ) );
hv = vector.clone();
hv.add( vptc ).normalize();
vv = hv.clone();
chv.y = 0;
hv.y = 0;
ha = Math.atan2( hv.z, hv.x ) - Math.atan2( chv.z, chv.x );
ha = ha > Math.PI ? ha - 2 * Math.PI : ha;
ha = ha < -Math.PI ? ha + 2 * Math.PI : ha;
va = Math.abs( cvv.angleTo( chv ) + ( cvv.y * vv.y <= 0 ? vv.angleTo( hv ) : -vv.angleTo( hv ) ) );
va *= vv.y < cvv.y ? 1 : -1;
return { left: ha, up: va };
},
/**
* Set control center
* @param {THREE.Vector3} vector - Vector to be looked at the center
*/
setControlCenter: function( vector = this.options.initialLookAt ) {
const { left, up } = this.calculateCameraDirectionDelta( vector );
this.rotateOrbitControl( left, up );
},
/**
* Tween control looking center
* @param {THREE.Vector3} vector - Vector to be looked at the center
* @param {number} [duration=1000] - Duration to tween
* @param {function} [easing=TWEEN.Easing.Exponential.Out] - Easing function
* @memberOf Viewer
* @instance
*/
tweenControlCenter: function ( vector, duration, easing ) {
if ( vector instanceof Array ) {
easing = vector[ 2 ];
duration = vector[ 1 ];
vector = vector[ 0 ];
}
duration = duration !== undefined ? duration : 1000;
easing = easing || TWEEN.Easing.Exponential.Out;
const MEPS = 10e-5;
const { left, up } = this.calculateCameraDirectionDelta( vector );
const rotateControlLeft = this.rotateControlLeft.bind( this );
const rotateControlUp = this.rotateControlUp.bind( this );
const ov = { left: 0, up: 0 };
const nv = { left: 0, up: 0 };
this.tweenLeftAnimation.stop();
this.tweenUpAnimation.stop();
this.tweenLeftAnimation = new TWEEN.Tween( ov )
.to( { left }, duration )
.easing( easing )
.onUpdate(function(ov){
const diff = ov.left - nv.left;
if( Math.abs( diff ) < MEPS ) this.tweenLeftAnimation.stop();
rotateControlLeft( diff );
nv.left = ov.left;
}.bind(this))
.start();
this.tweenUpAnimation = new TWEEN.Tween( ov )
.to( { up }, duration )
.easing( easing )
.onUpdate(function(ov){
const diff = ov.up - nv.up;
if( Math.abs( diff ) < MEPS ) this.tweenUpAnimation.stop();
rotateControlUp( diff );
nv.up = ov.up;
}.bind(this))
.start();
},
/**
* Tween control looking center by object
* @param {THREE.Object3D} object - Object to be looked at the center
* @param {number} [duration=1000] - Duration to tween
* @param {function} [easing=TWEEN.Easing.Exponential.Out] - Easing function
* @memberOf Viewer
* @instance
*/
tweenControlCenterByObject: function ( object, duration, easing ) {
this.tweenControlCenter( object.getWorldPosition( new THREE.Vector3() ), duration, easing );
},
/**
* This is called when window size is changed
* @fires Viewer#window-resize
* @param {number} [windowWidth] - Specify if custom element has changed width
* @param {number} [windowHeight] - Specify if custom element has changed height
* @memberOf Viewer
* @instance
*/
onWindowResize: function ( windowWidth, windowHeight ) {
let width, height;
const expand = this.container.classList.contains( EVENTS.CONTAINER ) || this.container.isFullscreen;
if ( windowWidth !== undefined && windowHeight !== undefined ) {
width = windowWidth;
height = windowHeight;
this.container._width = windowWidth;
this.container._height = windowHeight;
} else {
const adjustWidth = isAndroid
? Math.min(document.documentElement.clientWidth, window.innerWidth || 0)
: Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const adjustHeight = isAndroid
? Math.min(document.documentElement.clientHeight, window.innerHeight || 0)
: Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
width = expand ? adjustWidth : this.container.clientWidth;
height = expand ? adjustHeight : this.container.clientHeight;
this.container._width = width;
this.container._height = height;
}
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize( width, height );
// Update reticle
if ( this.options.enableReticle || this.tempEnableReticle ) {
this.updateReticleEvent();
}
/**
* Window resizing event
* @type {object}
* @event Viewer#window-resize
* @property {number} width - Width of the window
* @property {number} height - Height of the window
*/
this.dispatchEvent( { type: EVENTS.WIDNOW_RESIZE, width: width, height: height });
this.scene.traverse( function ( object ) {
if ( object.dispatchEvent ) {
object.dispatchEvent( { type: EVENTS.WIDNOW_RESIZE, width: width, height: height });
}
} );
},
/**
* Add output element
* @memberOf Viewer
* @instance
*/
addOutputElement: function () {
const element = document.createElement( 'div' );
element.style.position = 'absolute';
element.style.right = '10px';
element.style.top = '10px';
element.style.color = '#fff';
this.container.appendChild( element );
this.outputDivElement = element;
},
/**
* Output position in developer console by holding down Ctrl button
* @memberOf Viewer
* @instance
*/
outputPosition: function () {
const intersects = this.raycaster.intersectObject( this.panorama, true );
if ( intersects.length > 0 ) {
const point = intersects[ 0 ].point.clone();
const world = this.panorama.getWorldPosition( new THREE.Vector3() );
point.sub( world );
const message = `${point.x.toFixed(2)}, ${point.y.toFixed(2)}, ${point.z.toFixed(2)}`;
if ( point.length() === 0 ) { return; }
switch ( this.options.output ) {
case 'console':
console.info( message );
break;
case 'overlay':
this.outputDivElement.textContent = message;
break;
default:
break;
}
}
},
/**
* On mouse down
* @param {MouseEvent} event
* @memberOf Viewer
* @instance
*/
onMouseDown: function ( event ) {
event.preventDefault();
this.userMouse.x = ( event.clientX >= 0 ) ? event.clientX : event.touches[0].clientX;
this.userMouse.y = ( event.clientY >= 0 ) ? event.clientY : event.touches[0].clientY;
this.userMouse.type = 'mousedown';
this.onTap( event );
},
/**
* On mouse move
* @param {MouseEvent} event
* @memberOf Viewer
* @instance
*/
onMouseMove: function ( event ) {
event.preventDefault();
this.userMouse.type = 'mousemove';
this.onTap( event );
},
/**
* On mouse up
* @param {MouseEvent} event
* @memberOf Viewer
* @instance
*/
onMouseUp: function ( event ) {
let onTarget = false;
this.userMouse.type = 'mouseup';
const type = ( this.userMouse.x >= event.clientX - this.options.clickTolerance
&& this.userMouse.x <= event.clientX + this.options.clickTolerance
&& this.userMouse.y >= event.clientY - this.options.clickTolerance
&& this.userMouse.y <= event.clientY + this.options.clickTolerance )
|| ( event.changedTouches
&& this.userMouse.x >= event.changedTouches[0].clientX - this.options.clickTolerance
&& this.userMouse.x <= event.changedTouches[0].clientX + this.options.clickTolerance
&& this.userMouse.y >= event.changedTouches[0].clientY - this.options.clickTolerance
&& this.userMouse.y <= event.changedTouches[0].clientY + this.options.clickTolerance )
? 'click' : undefined;
// Event should happen on canvas
if ( event && event.target && !event.target.classList.contains( 'panolens-canvas' ) ) { return; }
event.preventDefault();
if ( event.changedTouches && event.changedTouches.length === 1 ) {
onTarget = this.onTap( { clientX: event.changedTouches[0].clientX, clientY: event.changedTouches[0].clientY }, type );
} else {
onTarget = this.onTap( event, type );
}
this.userMouse.type = 'none';
if ( onTarget ) {
return;
}
if ( type === 'click' ) {
const { options: { autoHideInfospot, autoHideControlBar }, panorama, toggleControlBar } = this;
if ( autoHideInfospot && panorama ) {
panorama.toggleInfospotVisibility();
}
if ( autoHideControlBar ) {
toggleControlBar();
}
}
},
/**
* On tap eveny frame
* @param {MouseEvent} event
* @param {string} type
* @memberOf Viewer
* @instance
*/
onTap: function ( event, type ) {
const { left, top } = this.container.getBoundingClientRect();
const { clientWidth, clientHeight } = this.container;
this.raycasterPoint.x = ( ( event.clientX - left ) / clientWidth ) * 2 - 1;
this.raycasterPoint.y = - ( ( event.clientY - top ) / clientHeight ) * 2 + 1;
this.raycaster.setFromCamera( this.raycasterPoint, this.camera );
// Return if no panorama
if ( !this.panorama ) {
return;
}
// output infospot information
if ( event.type !== 'mousedown' && this.touchSupported || this.outputEnabled ) {
this.outputPosition();
}
const intersects = this.raycaster.intersectObjects( this.panorama.children, true );
const intersect_entity = this.getConvertedIntersect( intersects );
const intersect = ( intersects.length > 0 ) ? intersects[0].object : undefined;
if ( this.userMouse.type === 'mouseup' ) {
if ( intersect_entity && this.pressEntityObject === intersect_entity && this.pressEntityObject.dispatchEvent ) {
this.pressEntityObject.dispatchEvent( { type: 'pressstop-entity', mouseEvent: event } );
}
this.pressEntityObject = undefined;
}
if ( this.userMouse.type === 'mouseup' ) {
if ( intersect && this.pressObject === intersect && this.pressObject.dispatchEvent ) {
this.pressObject.dispatchEvent( { type: 'pressstop', mouseEvent: event } );
}
this.pressObject = undefined;
}
if ( type === 'click' ) {
this.panorama.dispatchEvent( { type: 'click', intersects: intersects, mouseEvent: event } );
if ( intersect_entity && intersect_entity.dispatchEvent ) {
intersect_entity.dispatchEvent( { type: 'click-entity', mouseEvent: event } );
}
if ( intersect && intersect.dispatchEvent ) {
intersect.dispatchEvent( { type: 'click', mouseEvent: event } );
}
} else {
this.panorama.dispatchEvent( { type: 'hover', intersects: intersects, mouseEvent: event } );
if ( ( this.hoverObject && intersects.length > 0 && this.hoverObject !== intersect_entity )
|| ( this.hoverObject && intersects.length === 0 ) ){
if ( this.hoverObject.dispatchEvent ) {
this.hoverObject.dispatchEvent( { type: 'hoverleave', mouseEvent: event } );
this.reticle.end();
}
this.hoverObject = undefined;
}
if ( intersect_entity && intersects.length > 0 ) {
if ( this.hoverObject !== intersect_entity ) {
this.hoverObject = intersect_entity;
if ( this.hoverObject.dispatchEvent ) {
this.hoverObject.dispatchEvent( { type: 'hoverenter', mouseEvent: event } );
// Start reticle timer
if ( this.options.autoReticleSelect && this.options.enableReticle || this.tempEnableReticle ) {
this.reticle.start( this.onTap.bind( this, event, 'click' ) );
}
}
}
if ( this.userMouse.type === 'mousedown' && this.pressEntityObject != intersect_entity ) {
this.pressEntityObject = intersect_entity;
if ( this.pressEntityObject.dispatchEvent ) {
this.pressEntityObject.dispatchEvent( { type: 'pressstart-entity', mouseEvent: event } );
}
}
if ( this.userMouse.type === 'mousedown' && this.pressObject != intersect ) {
this.pressObject = intersect;
if ( this.pressObject.dispatchEvent ) {
this.pressObject.dispatchEvent( { type: 'pressstart', mouseEvent: event } );
}
}
if ( this.userMouse.type === 'mousemove' || this.options.enableReticle ) {
if ( intersect && intersect.dispatchEvent ) {
intersect.dispatchEvent( { type: 'hover', mouseEvent: event } );
}
if ( this.pressEntityObject && this.pressEntityObject.dispatchEvent ) {
this.pressEntityObject.dispatchEvent( { type: 'pressmove-entity', mouseEvent: event } );
}
if ( this.pressObject && this.pressObject.dispatchEvent ) {
this.pressObject.dispatchEvent( { type: 'pressmove', mouseEvent: event } );
}
}
}
if ( !intersect_entity && this.pressEntityObject && this.pressEntityObject.dispatchEvent ) {
this.pressEntityObject.dispatchEvent( { type: 'pressstop-entity', mouseEvent: event } );
this.pressEntityObject = undefined;
}
if ( !intersect && this.pressObject && this.pressObject.dispatchEvent ) {
this.pressObject.dispatchEvent( { type: 'pressstop', mouseEvent: event } );
this.pressObject = undefined;
}
}
// Infospot handler
if ( intersect && intersect instanceof Infospot ) {
this.infospot = intersect;
if ( type === 'click' ) {
return true;
}
} else if ( this.infospot ) {
this.hideInfospot();
}
// Auto rotate
if ( this.options.autoRotate && this.userMouse.type !== 'mousemove' ) {
// Auto-rotate idle timer
clearTimeout( this.autoRotateRequestId );
if ( this.control === this.OrbitControls ) {
this.OrbitControls.autoRotate = false;
this.autoRotateRequestId = window.setTimeout( this.enableAutoRate.bind( this ), this.options.autoRotateActivationDuration );
}
}
},
/**
* Get converted intersect
* @param {array} intersects
* @memberOf Viewer
* @instance
*/
getConvertedIntersect: function ( intersects ) {
let intersect;
for ( let i = 0; i < intersects.length; i++ ) {
if ( intersects[i].distance >= 0 && intersects[i].object && !intersects[i].object.passThrough ) {
if ( intersects[i].object.entity && intersects[i].object.entity.passThrough ) {
continue;
} else if ( intersects[i].object.entity && !intersects[i].object.entity.passThrough ) {
intersect = intersects[i].object.entity;
break;
} else {
intersect = intersects[i].object;
break;
}
}
}
return intersect;
},
/**
* Hide infospot
* @memberOf Viewer
* @instance
*/
hideInfospot: function () {
if ( this.infospot ) {
this.infospot.onHoverEnd();
this.infospot = undefined;
}
},
/**
* Toggle control bar
* @memberOf Viewer
* @instance
* @fires Viewer#control-bar-toggle
*/
toggleControlBar: function () {
const { widget } = this;
/**
* Toggle control bar event
* @type {object}
* @event Viewer#control-bar-toggle
*/
if ( widget ) {
widget.dispatchEvent( { type: 'control-bar-toggle' } );
}
},
/**
* On key down
* @param {KeyboardEvent} event
* @memberOf Viewer
* @instance
*/
onKeyDown: function ( event ) {
if ( this.options.output && this.options.output !== 'none' && event.key === 'Control' ) {
this.outputEnabled = true;
}
},
/**
* On key up
* @param {KeyboardEvent} event
* @memberOf Viewer
* @instance
*/
onKeyUp: function () {
this.outputEnabled = false;
},
/**
* Update control and callbacks
* @memberOf Viewer
* @instance
*/
update: function () {
const { scene, control, OrbitControls, DeviceOrientationControls } = this;
// Tween Update
TWEEN.update();
// Callbacks Update
this.updateCallbacks.forEach( callback => callback() );
// Control Update
if ( OrbitControls.enabled ) OrbitControls.update();
if ( control === DeviceOrientationControls ) {
DeviceOrientationControls.update(OrbitControls.spherical.theta);
}
// Infospot Update
const v3 = new THREE.Vector3();
scene.traverse( function( child ){
if ( child instanceof Infospot
&& child.element
&& ( this.hoverObject === child
|| child.element.style.display !== 'none'
|| (child.element.left && child.element.left.style.display !== 'none')
|| (child.element.right && child.element.right.style.display !== 'none') ) ) {
if ( this.checkSpriteInViewport( child ) ) {
const { x, y } = this.getScreenVector( child.getWorldPosition( v3 ) );
child.translateElement( x, y );
} else {
child.onDismiss();
}
}
}.bind( this ) );
},
/**
* Rendering function to be called on every animation frame
* Render reticle last
* @memberOf Viewer
* @instance
*/
render: function () {
if ( this.mode === MODES.CARDBOARD || this.mode === MODES.STEREO ) {
this.renderer.clear();
this.effect.render( this.scene, this.camera, this.panorama );
this.effect.render( this.sceneReticle, this.camera );
} else {
this.renderer.clear();
this.renderer.render( this.scene, this.camera );
this.renderer.clearDepth();
this.renderer.render( this.sceneReticle, this.camera );
}
},
/**
* Animate
* @memberOf Viewer
* @instance
*/
animate: function () {
this.requestAnimationId = window.requestAnimationFrame( this.animate.bind( this ) );
this.onChange();
},
/**
* On change
* @memberOf Viewer
* @instance
*/
onChange: function () {
this.update();
this.render();
},
/**
* Register mouse and touch event on container
* @memberOf Viewer
* @instance
*/
registerMouseAndTouchEvents: function () {
const options = { passive: false };
this.container.addEventListener( 'mousedown' , this.handlerMouseDown, options );
this.container.addEventListener( 'mousemove' , this.handlerMouseMove, options );
this.container.addEventListener( 'mouseup' , this.handlerMouseUp , options );
this.container.addEventListener( 'touchstart', this.handlerMouseDown, options );
this.container.addEventListener( 'touchend' , this.handlerMouseUp , options );
},
/**
* Unregister mouse and touch event on container
* @memberOf Viewer
* @instance
*/
unregisterMouseAndTouchEvents: function () {
this.container.removeEventListener( 'mousedown' , this.handlerMouseDown, false );
this.container.removeEventListener( 'mousemove' , this.handlerMouseMove, false );
this.container.removeEventListener( 'mouseup' , this.handlerMouseUp , false );
this.container.removeEventListener( 'touchstart', this.handlerMouseDown, false );
this.container.removeEventListener( 'touchend' , this.handlerMouseUp , false );
},
/**
* Register reticle event
* @memberOf Viewer
* @instance
*/
registerReticleEvent: function () {
this.addUpdateCallback( this.handlerTap );
},
/**
* Unregister reticle event
* @memberOf Viewer
* @instance
*/
unregisterReticleEvent: function () {
this.removeUpdateCallback( this.handlerTap );
},
/**
* Update reticle event
* @memberOf Viewer
* @instance
*/
updateReticleEvent: function () {
const clientX = this.container.clientWidth / 2 + this.container.offsetLeft;
const clientY = this.container.clientHeight / 2;
this.removeUpdateCallback( this.handlerTap );
this.handlerTap = this.onTap.bind( this, { clientX, clientY } );
this.addUpdateCallback( this.handlerTap );
},
/**
* Register container and window listeners
* @memberOf Viewer
* @instance
*/
registerEventListeners: function () {
// Resize Event
window.addEventListener( 'resize' , this.handlerWindowResize, true );
// Keyboard Event
window.addEventListener( 'keydown', this.handlerKeyDown, true );
window.addEventListener( 'keyup' , this.handlerKeyUp , true );
},
/**
* Unregister container and window listeners
* @memberOf Viewer
* @instance
*/
unregisterEventListeners: function () {
// Resize Event
window.removeEventListener( 'resize' , this.handlerWindowResize, true );
// Keyboard Event
window.removeEventListener( 'keydown', this.handlerKeyDown, true );
window.removeEventListener( 'keyup' , this.handlerKeyUp , true );
},
/**
* Dispose all scene objects and clear cache
* @memberOf Viewer
* @instance
*/
dispose: function () {
this.disableAutoRate();
this.tweenLeftAnimation.stop();
this.tweenUpAnimation.stop();
// Unregister dom event listeners
this.unregisterEventListeners();
// recursive disposal on 3d objects
function recursiveDispose ( object ) {
for ( let i = object.children.length - 1; i >= 0; i-- ) {
recursiveDispose( object.children[i] );
object.remove( object.children[i] );
}
if ( object instanceof Panorama || object instanceof Infospot ) {
object.dispose();
object = null;
} else if ( object.dispatchEvent ){
object.dispatchEvent( 'dispose' );
}
}
recursiveDispose( this.scene );
// dispose widget
if ( this.widget ) {
this.widget.dispose();
this.widget = null;
}
// clear cache
if ( THREE.Cache && THREE.Cache.enabled ) {
THREE.Cache.clear();
}
},
/**
* Destroy viewer by disposing and stopping requestAnimationFrame
* @memberOf Viewer
* @instance
*/
destroy: function () {
this.dispose();
this.render();
window.cancelAnimationFrame( this.requestAnimationId );
},
/**
* On panorama dispose
* @memberOf Viewer
* @instance
*/
onPanoramaDispose: function ( panorama ) {
const { scene } = this;
const infospotDisposeMapper = infospot => infospot.toPanorama !== panorama ? infospot : infospot.dispose();
if ( panorama instanceof VideoPanorama ) {
this.hideVideoWidget();
}
// traverse the scene to find association
scene.traverse( object => {
if ( object instanceof Panorama ) {
object.linkedSpots = object.linkedSpots.map( infospotDisposeMapper ).filter( infospot => !!infospot );
}
} );
if ( panorama === this.panorama ) {
this.panorama = null;
}
},
/**
* Load ajax call
* @param {string} url - URL to be requested
* @param {function} [callback] - Callback after request completes
* @memberOf Viewer
* @instance
*/
loadAsyncRequest: function ( url, callback = () => {} ) {
const request = new window.XMLHttpRequest();
request.onloadend = function ( event ) {
callback( event );
};
request.open( 'GET', url, true );
request.send( null );
},
/**
* View indicator in upper left
* @memberOf Viewer
* @instance
*/
addViewIndicator: function () {
const scope = this;
function loadViewIndicator ( asyncEvent ) {
if ( asyncEvent.loaded === 0 ) return;
const viewIndicatorDiv = asyncEvent.target.responseXML.documentElement;
viewIndicatorDiv.style.width = scope.viewIndicatorSize + 'px';
viewIndicatorDiv.style.height = scope.viewIndicatorSize + 'px';
viewIndicatorDiv.style.position = 'absolute';
viewIndicatorDiv.style.top = '10px';
viewIndicatorDiv.style.left = '10px';
viewIndicatorDiv.style.opacity = '0.5';
viewIndicatorDiv.style.cursor = 'pointer';
viewIndicatorDiv.id = 'panolens-view-indicator-container';
scope.container.appendChild( viewIndicatorDiv );
const indicator = viewIndicatorDiv.querySelector( '#indicator' );
const setIndicatorD = function () {
scope.radius = scope.viewIndicatorSize * 0.225;
scope.currentPanoAngle = scope.camera.rotation.y - THREE.Math.degToRad( 90 );
scope.fovAngle = THREE.Math.degToRad( scope.camera.fov ) ;
scope.leftAngle = -scope.currentPanoAngle - scope.fovAngle / 2;
scope.rightAngle = -scope.currentPanoAngle + scope.fovAngle / 2;
scope.leftX = scope.radius * Math.cos( scope.leftAngle );
scope.leftY = scope.radius * Math.sin( scope.leftAngle );
scope.rightX = scope.radius * Math.cos( scope.rightAngle );
scope.rightY = scope.radius * Math.sin( scope.rightAngle );
scope.indicatorD = 'M ' + scope.leftX + ' ' + scope.leftY + ' A ' + scope.radius + ' ' + scope.radius + ' 0 0 1 ' + scope.rightX + ' ' + scope.rightY;
if ( scope.leftX && scope.leftY && scope.rightX && scope.rightY && scope.radius ) {
indicator.setAttribute( 'd', scope.indicatorD );
}
};
scope.addUpdateCallback( setIndicatorD );
const indicatorOnMouseEnter = function () {
this.style.opacity = '1';
};
const indicatorOnMouseLeave = function () {
this.style.opacity = '0.5';
};
viewIndicatorDiv.addEventListener( 'mouseenter', indicatorOnMouseEnter );
viewIndicatorDiv.addEventListener( 'mouseleave', indicatorOnMouseLeave );
}
this.loadAsyncRequest( DataImage.ViewIndicator, loadViewIndicator );
},
/**
* Append custom control item to existing control bar
* @param {object} [option={}] - Style object to overwirte default element style. It takes 'style', 'onTap' and 'group' properties.
* @memberOf Viewer
* @instance
*/
appendControlItem: function ( option ) {
const item = this.widget.createCustomItem( option );
if ( option.group === 'video' ) {
this.widget.videoElement.appendChild( item );
} else {
this.widget.barElement.appendChild( item );
}
return item;
},
/**
* Remove item within the control bar
* @param {HTMLElement} item item to be removed
*/
removeControlItem: function( item ) {
const { barElement, videoElement } = this.widget;
const barElements = Array.prototype.slice.call( barElement.children );
const videoElements = Array.prototype.slice.call( videoElement.children );
if ( barElements.includes( item ) ) barElement.removeChild( item );
if ( videoElements.includes( item ) ) videoElement.removeChild( item );
},
/**
* Clear all cached files
* @memberOf Viewer
* @instance
*/
clearAllCache: function () {
THREE.Cache.clear();
}
} );
export { Viewer };