( function () { 'use strict'; /** * 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.reverseDragging=false] - Reverse dragging direction * @param {boolean} [options.enableReticle=false] - Enable reticle for mouseless interaction other than VR mode * @param {number} [options.dwellTime=1500] - Dwell time for reticle selection * @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='none'] - Whether and where to output raycast position. Could be 'console' or 'overlay' */ PANOLENS.Viewer = function ( options ) { THREE.EventDispatcher.call( this ); if ( !THREE ) { console.error('Three.JS not found'); return; } var container; options = options || {}; options.controlBar = options.controlBar !== undefined ? options.controlBar : true; options.controlButtons = options.controlButtons || [ 'fullscreen', 'setting', 'video' ]; options.autoHideControlBar = options.autoHideControlBar !== undefined ? options.autoHideControlBar : false; options.autoHideInfospot = options.autoHideInfospot !== undefined ? options.autoHideInfospot : true; options.horizontalView = options.horizontalView !== undefined ? options.horizontalView : false; options.clickTolerance = options.clickTolerance || 10; options.cameraFov = options.cameraFov || 60; options.reverseDragging = options.reverseDragging || false; options.enableReticle = options.enableReticle || false; options.dwellTime = options.dwellTime || 1500; options.autoReticleSelect = options.autoReticleSelect !== undefined ? options.autoReticleSelect : true; options.viewIndicator = options.viewIndicator !== undefined ? options.viewIndicator : false; options.indicatorSize = options.indicatorSize || 30; options.output = options.output ? options.output : 'none'; this.options = options; // Container if ( options.container ) { container = options.container; container._width = container.clientWidth; container._height = container.clientHeight; } else { container = document.createElement( 'div' ); container.classList.add( 'panolens-container' ); container.style.width = '100%'; container.style.height = '100%'; container._width = window.innerWidth; container._height = window.innerHeight; document.body.appendChild( container ); } this.container = container; this.camera = options.camera || new THREE.PerspectiveCamera( this.options.cameraFov, this.container.clientWidth / this.container.clientHeight, 1, 10000 ); this.scene = options.scene || new THREE.Scene(); this.renderer = options.renderer || new THREE.WebGLRenderer( { alpha: true, antialias: false } ); this.viewIndicatorSize = options.indicatorSize; this.reticle = {}; this.tempEnableReticle = this.options.enableReticle; this.mode = PANOLENS.Modes.NORMAL; this.OrbitControls; this.DeviceOrientationControls; this.CardboardEffect; this.StereoEffect; this.controls; this.effect; this.panorama; this.widget; this.hoverObject; this.infospot; this.pressEntityObject; this.pressObject; this.raycaster = new THREE.Raycaster(); this.raycasterPoint = new THREE.Vector2(); this.userMouse = new THREE.Vector2(); this.updateCallbacks = []; this.requestAnimationId; this.cameraFrustum = new THREE.Frustum(); this.cameraViewProjectionMatrix = new THREE.Matrix4(); this.outputDivElement; // Handler references this.HANDLER_MOUSE_DOWN = this.onMouseDown.bind( this ); this.HANDLER_MOUSE_UP = this.onMouseUp.bind( this ); this.HANDLER_MOUSE_MOVE = this.onMouseMove.bind( this ); this.HANDLER_WINDOW_RESIZE = this.onWindowResize.bind( this ); this.HANDLER_KEY_DOWN = this.onKeyDown.bind( this ); this.HANDLER_KEY_UP = this.onKeyUp.bind( this ); this.HANDLER_TAP = this.onTap.bind( this, { clientX: this.container.clientWidth / 2, clientY: this.container.clientHeight / 2 } ); // Flag for infospot output this.OUTPUT_INFOSPOT = false; // Animations this.tweenLeftAnimation = new TWEEN.Tween(); this.tweenUpAnimation = new TWEEN.Tween(); // Renderer this.renderer.setPixelRatio( window.devicePixelRatio ); this.renderer.setSize( this.container.clientWidth, this.container.clientHeight ); this.renderer.setClearColor( 0x000000, 1 ); this.renderer.sortObjects = false; // Append Renderer Element to container this.renderer.domElement.classList.add( 'panolens-canvas' ); this.renderer.domElement.style.display = 'block'; this.container.style.backgroundColor = '#000'; this.container.appendChild( this.renderer.domElement ); // Camera Controls this.OrbitControls = new THREE.OrbitControls( this.camera, this.container ); this.OrbitControls.name = 'orbit'; this.OrbitControls.minDistance = 1; this.OrbitControls.noPan = true; this.DeviceOrientationControls = new THREE.DeviceOrientationControls( this.camera, this.container ); this.DeviceOrientationControls.name = 'device-orientation'; this.DeviceOrientationControls.enabled = false; this.camera.position.z = 1; // Register change event if passiveRenering if ( this.options.passiveRendering ) { console.warn( 'passiveRendering is now deprecated' ); } // Controls this.controls = [ this.OrbitControls, this.DeviceOrientationControls ]; this.control = this.OrbitControls; // Cardboard effect this.CardboardEffect = new THREE.CardboardEffect( this.renderer ); this.CardboardEffect.setSize( this.container.clientWidth, this.container.clientHeight ); // Stereo effect this.StereoEffect = new THREE.StereoEffect( this.renderer ); this.StereoEffect.setSize( this.container.clientWidth, this.container.clientHeight ); this.effect = this.CardboardEffect; // Add default hidden reticle this.addReticle(); // Lock horizontal view if ( this.options.horizontalView ) { this.OrbitControls.minPolarAngle = Math.PI / 2; this.OrbitControls.maxPolarAngle = Math.PI / 2; } // Add Control UI if ( this.options.controlBar !== false ) { this.addDefaultControlBar( this.options.controlButtons ); } // Add View Indicator if ( this.options.viewIndicator ) { this.addViewIndicator(); } // Reverse dragging direction if ( this.options.reverseDragging ) { this.reverseDraggingDirection(); } // Register event if reticle is enabled, otherwise defaults to mouse if ( this.options.enableReticle ) { this.enableReticleControl(); } else { this.registerMouseAndTouchEvents(); } if ( this.options.output === 'overlay' ) { this.addOutputElement(); } // Register dom event listeners this.registerEventListeners(); // Animate this.animate.call( this ); }; PANOLENS.Viewer.prototype = Object.create( THREE.EventDispatcher.prototype ); PANOLENS.Viewer.prototype.constructor = PANOLENS.Viewer; /** * 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 */ PANOLENS.Viewer.prototype.add = function ( object ) { if ( arguments.length > 1 ) { for ( var i = 0; i < arguments.length; i ++ ) { this.add( arguments[ i ] ); } return this; } this.scene.add( object ); // All object added to scene has 'panolens-viewer-handler' event to handle viewer communication if ( object.addEventListener ) { object.addEventListener( 'panolens-viewer-handler', this.eventHandler.bind( this ) ); } // All object added to scene being passed with container if ( object instanceof PANOLENS.Panorama && object.dispatchEvent ) { object.dispatchEvent( { type: 'panolens-container', container: this.container } ); } // Hookup default panorama event listeners if ( object.type === 'panorama' ) { this.addPanoramaEventListener( object ); if ( !this.panorama ) { this.setPanorama( object ); } } }; /** * Remove an object from the scene * @param {THREE.Object3D} object - Object to be removed */ PANOLENS.Viewer.prototype.remove = function ( object ) { if ( object.removeEventListener ) { object.removeEventListener( 'panolens-viewer-handler', this.eventHandler.bind( this ) ); } this.scene.remove( object ); }; /** * Add default control bar * @param {array} array - The control buttons array */ PANOLENS.Viewer.prototype.addDefaultControlBar = function ( array ) { var scope = this; if ( this.widget ) { console.warn( 'Default control bar exists' ); return; } this.widget = new PANOLENS.Widget( this.container ); this.widget.addEventListener( 'panolens-viewer-handler', this.eventHandler.bind( this ) ); this.widget.addControlBar(); array.forEach( function( buttonName ){ scope.widget.addControlButton( buttonName ); } ); }; /** * Set a panorama to be the current one * @param {PANOLENS.Panorama} pano - Panorama to be set */ PANOLENS.Viewer.prototype.setPanorama = function ( pano ) { var scope = this, leavingPanorama = this.panorama; if ( pano.type === 'panorama' && leavingPanorama !== pano ) { // Clear exisiting infospot this.hideInfospot(); var afterEnterComplete = function () { leavingPanorama && leavingPanorama.onLeave(); pano.removeEventListener( 'enter-fade-start', afterEnterComplete ); }; pano.addEventListener( 'enter-fade-start', afterEnterComplete ); // Assign and enter panorama (this.panorama = pano).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 */ PANOLENS.Viewer.prototype.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 */ PANOLENS.Viewer.prototype.dispatchEventToChildren = function ( event ) { this.scene.traverse( function ( object ) { if ( object.dispatchEvent ) { object.dispatchEvent( event ); } }); }; /** * Set widget content * @param {integer} controlIndex - Control index * @param {PANOLENS.Modes} mode - Modes for effects */ PANOLENS.Viewer.prototype.activateWidgetItem = function ( controlIndex, mode ) { var mainMenu = this.widget.mainMenu; var ControlMenuItem = mainMenu.children[ 0 ]; var ModeMenuItem = mainMenu.children[ 1 ]; var 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 PANOLENS.Modes.CARDBOARD: item = ModeMenuItem.subMenu.children[ 2 ]; break; case PANOLENS.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 {PANOLENS.Modes} mode - Modes for effects */ PANOLENS.Viewer.prototype.enableEffect = function ( mode ) { if ( this.mode === mode ) { return; } if ( mode === PANOLENS.Modes.NORMAL ) { this.disableEffect(); return; } else { this.mode = mode; } var fov = this.camera.fov; switch( mode ) { case PANOLENS.Modes.CARDBOARD: this.effect = this.CardboardEffect; this.enableReticleControl(); break; case PANOLENS.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 PANOLENS.Viewer#panolens-dual-eye-effect * @event PANOLENS.Infospot#panolens-dual-eye-effect * @property {PANOLENS.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; }; /** * Disable additional rendering effect */ PANOLENS.Viewer.prototype.disableEffect = function () { if ( this.mode === PANOLENS.Modes.NORMAL ) { return; } this.mode = PANOLENS.Modes.NORMAL; this.disableReticleControl(); this.activateWidgetItem( undefined, this.mode ); /** * Dual eye effect event * @type {object} * @event PANOLENS.Viewer#panolens-dual-eye-effect * @event PANOLENS.Infospot#panolens-dual-eye-effect * @property {PANOLENS.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(); }; /** * Enable reticle control */ PANOLENS.Viewer.prototype.enableReticleControl = function () { if ( this.reticle.visible ) { return; } if ( !this.reticle.textureLoaded ) { this.reticle.loadTextures(); } this.tempEnableReticle = true; // Register reticle event and unregister mouse event this.unregisterMouseAndTouchEvents(); this.reticle.show(); this.registerReticleEvent(); this.updateReticleEvent(); }; /** * Disable reticle control */ PANOLENS.Viewer.prototype.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(); } }; /** * Toggle video play or stop * @fires PANOLENS.Viewer#video-toggle */ PANOLENS.Viewer.prototype.toggleVideoPlay = function ( pause ) { if ( this.panorama instanceof PANOLENS.VideoPanorama ) { /** * Toggle video event * @type {object} * @event PANOLENS.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 * @fires PANOLENS.Viewer#video-time */ PANOLENS.Viewer.prototype.setVideoCurrentTime = function ( percentage ) { if ( this.panorama instanceof PANOLENS.VideoPanorama ) { /** * Setting video time event * @type {object} * @event PANOLENS.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 * @fires PANOLENS.Viewer#video-update */ PANOLENS.Viewer.prototype.onVideoUpdate = function ( percentage ) { /** * Video update event * @type {object} * @event PANOLENS.Viewer#video-update * @property {number} percentage - Percentage of a video. Range from 0.0 to 1.0 */ this.widget && this.widget.dispatchEvent( { type: 'video-update', percentage: percentage } ); }; /** * Add update callback to be called every animation frame */ PANOLENS.Viewer.prototype.addUpdateCallback = function ( fn ) { if ( fn ) { this.updateCallbacks.push( fn ); } }; /** * Remove update callback * @param {Function} fn - The function to be removed */ PANOLENS.Viewer.prototype.removeUpdateCallback = function ( fn ) { var index = this.updateCallbacks.indexOf( fn ); if ( fn && index >= 0 ) { this.updateCallbacks.splice( index, 1 ); } }; /** * Show video widget */ PANOLENS.Viewer.prototype.showVideoWidget = function () { /** * Show video widget event * @type {object} * @event PANOLENS.Viewer#video-control-show */ this.widget && this.widget.dispatchEvent( { type: 'video-control-show' } ); }; /** * Hide video widget */ PANOLENS.Viewer.prototype.hideVideoWidget = function () { /** * Hide video widget * @type {object} * @event PANOLENS.Viewer#video-control-hide */ this.widget && this.widget.dispatchEvent( { type: 'video-control-hide' } ); }; PANOLENS.Viewer.prototype.updateVideoPlayButton = function ( paused ) { if ( this.widget && this.widget.videoElement && this.widget.videoElement.controlButton ) { this.widget.videoElement.controlButton.update( paused ); } }; /** * Add default panorama event listeners * @param {PANOLENS.Panorama} pano - The panorama to be added with event listener */ PANOLENS.Viewer.prototype.addPanoramaEventListener = function ( pano ) { var scope = this; // Set camera control on every panorama pano.addEventListener( 'enter-fade-start', this.setCameraControl.bind( this ) ); // Show and hide widget event only when it's PANOLENS.VideoPanorama if ( pano instanceof PANOLENS.VideoPanorama ) { pano.addEventListener( 'enter-fade-start', this.showVideoWidget.bind( this ) ); pano.addEventListener( 'leave', function () { if ( !(this.panorama instanceof PANOLENS.VideoPanorama) ) { this.hideVideoWidget.call( this ); } }.bind( this ) ); } }; /** * Set camera control */ PANOLENS.Viewer.prototype.setCameraControl = function () { this.OrbitControls.target.copy( this.panorama.position ); }; /** * Get current camera control * @return {object} - Current navigation control. THREE.OrbitControls or THREE.DeviceOrientationControls */ PANOLENS.Viewer.prototype.getControl = function () { return this.control; }, /** * Get scene * @return {THREE.Scene} - Current scene which the viewer is built on */ PANOLENS.Viewer.prototype.getScene = function () { return this.scene; }; /** * Get camera * @return {THREE.Camera} - The scene camera */ PANOLENS.Viewer.prototype.getCamera = function () { return this.camera; }, /** * Get renderer * @return {THREE.WebGLRenderer} - The renderer using webgl */ PANOLENS.Viewer.prototype.getRenderer = function () { return this.renderer; }; /** * Get container * @return {HTMLDOMElement} - The container holds rendererd canvas */ PANOLENS.Viewer.prototype.getContainer = function () { return this.container; }; /** * Get control name * @return {string} - Control name. 'orbit' or 'device-orientation' */ PANOLENS.Viewer.prototype.getControlName = function () { return this.control.name; }; /** * Get next navigation control name * @return {string} - Next control name */ PANOLENS.Viewer.prototype.getNextControlName = function () { return this.controls[ this.getNextControlIndex() ].name; }; /** * Get next navigation control index * @return {number} - Next control index */ PANOLENS.Viewer.prototype.getNextControlIndex = function () { var controls, control, nextIndex; controls = this.controls; control = this.control; nextIndex = controls.indexOf( control ) + 1; return ( nextIndex >= controls.length ) ? 0 : nextIndex; }; /** * Set field of view of camera */ PANOLENS.Viewer.prototype.setCameraFov = function ( fov ) { this.camera.fov = fov; this.camera.updateProjectionMatrix(); }; /** * Enable control by index * @param {PANOLENS.Controls} index - Index of camera control */ PANOLENS.Viewer.prototype.enableControl = function ( index ) { index = ( index >= 0 && index < this.controls.length ) ? index : 0; this.control.enabled = false; this.control = this.controls[ index ]; this.control.enabled = true; switch ( index ) { case PANOLENS.Controls.ORBIT: this.camera.position.copy( this.panorama.position ); this.camera.position.z += 1; break; case PANOLENS.Controls.DEVICEORIENTATION: this.camera.position.copy( this.panorama.position ); break; default: break; } this.control.update(); this.activateWidgetItem( index, undefined ); }; /** * Disable current control */ PANOLENS.Viewer.prototype.disableControl = function () { this.control.enabled = false; }; /** * Toggle next control */ PANOLENS.Viewer.prototype.toggleNextControl = function () { this.enableControl( this.getNextControlIndex() ); }; /** * Screen Space Projection */ PANOLENS.Viewer.prototype.getScreenVector = function ( worldVector ) { var vector = worldVector.clone(); var widthHalf = ( this.container.clientWidth ) / 2; var 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 */ PANOLENS.Viewer.prototype.checkSpriteInViewport = function ( sprite ) { this.camera.matrixWorldInverse.getInverse( this.camera.matrixWorld ); this.cameraViewProjectionMatrix.multiplyMatrices( this.camera.projectionMatrix, this.camera.matrixWorldInverse ); this.cameraFrustum.setFromMatrix( this.cameraViewProjectionMatrix ); return sprite.visible && this.cameraFrustum.intersectsSprite( sprite ); }; /** * Reverse dragging direction */ PANOLENS.Viewer.prototype.reverseDraggingDirection = function () { this.OrbitControls.rotateSpeed *= -1; this.OrbitControls.momentumScalingFactor *= -1; }; /** * Add reticle */ PANOLENS.Viewer.prototype.addReticle = function () { this.reticle = new PANOLENS.Reticle( 0xffffff, this.options.autoReticleSelect, PANOLENS.DataImage.ReticleIdle, PANOLENS.DataImage.ReticleDwell, this.options.dwellTime, 45 ); this.reticle.position.z = -10; this.camera.add( this.reticle ); this.scene.add( this.camera ); }; /** * 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 */ PANOLENS.Viewer.prototype.tweenControlCenter = function ( vector, duration, easing ) { if ( this.control !== this.OrbitControls ) { return; } // Pass in arguments as array if ( vector instanceof Array ) { duration = vector[ 1 ]; easing = vector[ 2 ]; vector = vector[ 0 ]; } duration = duration !== undefined ? duration : 1000; easing = easing || TWEEN.Easing.Exponential.Out; var scope, ha, va, chv, cvv, hv, vv, vptc, ov, nv; scope = this; chv = this.camera.getWorldDirection(); cvv = chv.clone(); vptc = this.panorama.getWorldPosition().sub( this.camera.getWorldPosition() ); hv = vector.clone(); // Scale effect hv.x *= -1; 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; ov = { left: 0, up: 0 }; nv = { left: 0, up: 0 }; this.tweenLeftAnimation.stop(); this.tweenUpAnimation.stop(); this.tweenLeftAnimation = new TWEEN.Tween( ov ) .to( { left: ha }, duration ) .easing( easing ) .onUpdate(function(){ scope.control.rotateLeft( this.left - nv.left ); nv.left = this.left; }) .start(); this.tweenUpAnimation = new TWEEN.Tween( ov ) .to( { up: va }, duration ) .easing( easing ) .onUpdate(function(){ scope.control.rotateUp( this.up - nv.up ); nv.up = this.up; }) .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 */ PANOLENS.Viewer.prototype.tweenControlCenterByObject = function ( object, duration, easing ) { var isUnderScalePlaceHolder = false; object.traverseAncestors( function ( ancestor ) { if ( ancestor.scalePlaceHolder ) { isUnderScalePlaceHolder = true; } } ); if ( isUnderScalePlaceHolder ) { var invertXVector = new THREE.Vector3( -1, 1, 1 ); this.tweenControlCenter( object.getWorldPosition().multiply( invertXVector ), duration, easing ); } else { this.tweenControlCenter( object.getWorldPosition(), duration, easing ); } }; /** * This is called when window size is changed * @fires PANOLENS.Viewer#window-resize * @param {number} [windowWidth] - Specify if custom element has changed width * @param {number} [windowHeight] - Specify if custom element has changed height */ PANOLENS.Viewer.prototype.onWindowResize = function ( windowWidth, windowHeight ) { var width, height, expand; expand = this.container.classList.contains( 'panolens-container' ) || this.container.isFullscreen; if ( windowWidth !== undefined && windowHeight !== undefined ) { width = windowWidth; height = windowHeight; this.container._width = windowWidth; this.container._height = windowHeight; } else { width = expand ? Math.max(document.documentElement.clientWidth, window.innerWidth || 0) : this.container.clientWidth; height = expand ? Math.max(document.documentElement.clientHeight, window.innerHeight || 0) : 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 PANOLENS.Viewer#window-resize * @property {number} width - Width of the window * @property {number} height - Height of the window */ this.dispatchEvent( { type: 'window-resize', width: width, height: height }); this.scene.traverse( function ( object ) { if ( object.dispatchEvent ) { object.dispatchEvent( { type: 'window-resize', width: width, height: height }); } } ); }; PANOLENS.Viewer.prototype.addOutputElement = function () { var 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 infospot attach position in developer console by holding down Ctrl button */ PANOLENS.Viewer.prototype.outputInfospotPosition = function () { var intersects, point, panoramaWorldPosition, outputPosition; intersects = this.raycaster.intersectObject( this.panorama, true ); if ( intersects.length > 0 ) { point = intersects[0].point; panoramaWorldPosition = this.panorama.getWorldPosition(); // Panorama is scaled -1 on X axis outputPosition = new THREE.Vector3( -(point.x - panoramaWorldPosition.x).toFixed(2), (point.y - panoramaWorldPosition.y).toFixed(2), (point.z - panoramaWorldPosition.z).toFixed(2) ); switch ( this.options.output ) { case 'console': console.info( outputPosition.x + ', ' + outputPosition.y + ', ' + outputPosition.z ); break; case 'overlay': this.outputDivElement.textContent = outputPosition.x + ', ' + outputPosition.y + ', ' + outputPosition.z; break; default: break; } } }; PANOLENS.Viewer.prototype.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 ); }; PANOLENS.Viewer.prototype.onMouseMove = function ( event ) { event.preventDefault(); this.userMouse.type = 'mousemove'; this.onTap( event ); }; PANOLENS.Viewer.prototype.onMouseUp = function ( event ) { var onTarget = false, type; this.userMouse.type = 'mouseup'; 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' ) { this.options.autoHideInfospot && this.panorama && this.panorama.toggleInfospotVisibility(); this.options.autoHideControlBar && this.toggleControlBar(); } }; PANOLENS.Viewer.prototype.onTap = function ( event, type ) { var intersects, intersect_entity, intersect; this.raycasterPoint.x = ( ( event.clientX - this.container.offsetLeft ) / this.container.clientWidth ) * 2 - 1; this.raycasterPoint.y = - ( ( event.clientY - this.container.offsetTop ) / this.container.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' && PANOLENS.Utils.checkTouchSupported() || this.OUTPUT_INFOSPOT ) { this.outputInfospotPosition(); } intersects = this.raycaster.intersectObjects( this.panorama.children, true ); intersect_entity = this.getConvertedIntersect( intersects ); intersect = ( intersects.length > 0 ) ? intersects[0].object : intersect; 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 } ); // Cancel dwelling this.reticle.cancelDwelling(); } 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.startDwelling( 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 PANOLENS.Infospot ) { this.infospot = intersect; if ( type === 'click' ) { return true; } } else if ( this.infospot ) { this.hideInfospot(); } }; PANOLENS.Viewer.prototype.getConvertedIntersect = function ( intersects ) { var intersect; for ( var 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; }; PANOLENS.Viewer.prototype.hideInfospot = function ( intersects ) { if ( this.infospot ) { this.infospot.onHoverEnd(); this.infospot = undefined; } }; /** * Toggle control bar * @fires [PANOLENS.Viewer#control-bar-toggle] */ PANOLENS.Viewer.prototype.toggleControlBar = function () { /** * Toggle control bar event * @type {object} * @event PANOLENS.Viewer#control-bar-toggle */ this.widget && this.widget.dispatchEvent( { type: 'control-bar-toggle' } ); }; PANOLENS.Viewer.prototype.onKeyDown = function ( event ) { if ( this.options.output && this.options.output !== 'none' && event.key === 'Control' ) { this.OUTPUT_INFOSPOT = true; } }; PANOLENS.Viewer.prototype.onKeyUp = function ( event ) { this.OUTPUT_INFOSPOT = false; }; /** * Update control and callbacks */ PANOLENS.Viewer.prototype.update = function () { TWEEN.update(); this.updateCallbacks.forEach( function( callback ){ callback(); } ); this.control.update(); this.scene.traverse( function( child ){ if ( child instanceof PANOLENS.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 ) ) { var vector = this.getScreenVector( child.getWorldPosition() ); child.translateElement( vector.x, vector.y ); } else { child.onDismiss(); } } }.bind(this) ); }; /** * Rendering function to be called on every animation frame */ PANOLENS.Viewer.prototype.render = function () { if ( this.mode === PANOLENS.Modes.CARDBOARD || this.mode === PANOLENS.Modes.STEREO ) { this.effect.render( this.scene, this.camera ); } else { this.renderer.render( this.scene, this.camera ); } }; PANOLENS.Viewer.prototype.animate = function () { this.requestAnimationId = window.requestAnimationFrame( this.animate.bind( this ) ); this.onChange(); }; PANOLENS.Viewer.prototype.onChange = function () { this.update(); this.render(); }; /** * Register mouse and touch event on container */ PANOLENS.Viewer.prototype.registerMouseAndTouchEvents = function () { this.container.addEventListener( 'mousedown' , this.HANDLER_MOUSE_DOWN, false ); this.container.addEventListener( 'mousemove' , this.HANDLER_MOUSE_MOVE, false ); this.container.addEventListener( 'mouseup' , this.HANDLER_MOUSE_UP , false ); this.container.addEventListener( 'touchstart', this.HANDLER_MOUSE_DOWN, false ); this.container.addEventListener( 'touchend' , this.HANDLER_MOUSE_UP , false ); }; /** * Unregister mouse and touch event on container */ PANOLENS.Viewer.prototype.unregisterMouseAndTouchEvents = function () { this.container.removeEventListener( 'mousedown' , this.HANDLER_MOUSE_DOWN, false ); this.container.removeEventListener( 'mousemove' , this.HANDLER_MOUSE_MOVE, false ); this.container.removeEventListener( 'mouseup' , this.HANDLER_MOUSE_UP , false ); this.container.removeEventListener( 'touchstart', this.HANDLER_MOUSE_DOWN, false ); this.container.removeEventListener( 'touchend' , this.HANDLER_MOUSE_UP , false ); }; /** * Register reticle event */ PANOLENS.Viewer.prototype.registerReticleEvent = function () { this.addUpdateCallback( this.HANDLER_TAP ); }; /** * Unregister reticle event */ PANOLENS.Viewer.prototype.unregisterReticleEvent = function () { this.removeUpdateCallback( this.HANDLER_TAP ); }; /** * Update reticle event */ PANOLENS.Viewer.prototype.updateReticleEvent = function () { var centerX, centerY; centerX = this.container.clientWidth / 2 + this.container.offsetLeft; centerY = this.container.clientHeight / 2; this.removeUpdateCallback( this.HANDLER_TAP ); this.HANDLER_TAP = this.onTap.bind( this, { clientX: centerX, clientY: centerY } ); this.addUpdateCallback( this.HANDLER_TAP ); }; /** * Register container and window listeners */ PANOLENS.Viewer.prototype.registerEventListeners = function () { // Resize Event window.addEventListener( 'resize' , this.HANDLER_WINDOW_RESIZE, true ); // Keyboard Event window.addEventListener( 'keydown', this.HANDLER_KEY_DOWN, true ); window.addEventListener( 'keyup' , this.HANDLER_KEY_UP , true ); }; /** * Unregister container and window listeners */ PANOLENS.Viewer.prototype.unregisterEventListeners = function () { // Resize Event window.removeEventListener( 'resize' , this.HANDLER_WINDOW_RESIZE, true ); // Keyboard Event window.removeEventListener( 'keydown', this.HANDLER_KEY_DOWN, true ); window.removeEventListener( 'keyup' , this.HANDLER_KEY_UP , true ); }; /** * Dispose all scene objects and clear cache */ PANOLENS.Viewer.prototype.dispose = function () { // Unregister dom event listeners this.unregisterEventListeners(); // recursive disposal on 3d objects function recursiveDispose ( object ) { for ( var i = object.children.length - 1; i >= 0; i-- ) { recursiveDispose( object.children[i] ); object.remove( object.children[i] ); } if ( object instanceof PANOLENS.Infospot ) { object.dispose(); } object.geometry && object.geometry.dispose(); object.material && object.material.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(); } }; /** * Destory viewer by disposing and stopping requestAnimationFrame */ PANOLENS.Viewer.prototype.destory = function () { this.dispose(); this.render(); window.cancelAnimationFrame( this.requestAnimationId ); }; /** * On panorama dispose */ PANOLENS.Viewer.prototype.onPanoramaDispose = function ( panorama ) { if ( panorama instanceof PANOLENS.VideoPanorama ) { this.hideVideoWidget(); } if ( panorama === this.panorama ) { this.panorama = null; } }; /** * Load ajax call * @param {string} url - URL to be requested * @param {function} [callback] - Callback after request completes */ PANOLENS.Viewer.prototype.loadAsyncRequest = function ( url, callback ) { var request = new XMLHttpRequest(); request.onloadend = function ( event ) { callback && callback( event ); }; request.open( "GET", url, true ); request.send( null ); }; /** * View indicator in upper left * */ PANOLENS.Viewer.prototype.addViewIndicator = function () { var scope = this; function loadViewIndicator ( asyncEvent ) { if ( asyncEvent.loaded === 0 ) return; var 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 ); var indicator = viewIndicatorDiv.querySelector( "#indicator" ); var 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 ); var indicatorOnMouseEnter = function () { this.style.opacity = "1"; }; var indicatorOnMouseLeave = function () { this.style.opacity = "0.5"; }; viewIndicatorDiv.addEventListener( "mouseenter", indicatorOnMouseEnter ); viewIndicatorDiv.addEventListener( "mouseleave", indicatorOnMouseLeave ); } this.loadAsyncRequest( PANOLENS.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. */ PANOLENS.Viewer.prototype.appendControlItem = function ( option ) { var item = this.widget.createCustomItem( option ); if ( option.group === 'video' ) { this.widget.videoElement.appendChild( item ); } else { this.widget.barElement.appendChild( item ); } return item; }; } )();