UNPKG

panolens

Version:
685 lines (468 loc) 17 kB
import * as THREE from 'three'; import { DataImage } from '../DataImage'; import { MODES } from '../Constants'; import { TextureLoader } from '../loaders/TextureLoader'; import TWEEN from '@tweenjs/tween.js'; /** * @classdesc Information spot attached to panorama * @constructor * @param {number} [scale=300] - Default scale * @param {string} [imageSrc=PANOLENS.DataImage.Info] - Image overlay info * @param {boolean} [animated=true] - Enable default hover animation */ function Infospot ( scale = 300, imageSrc, animated ) { const duration = 500, scaleFactor = 1.3; imageSrc = imageSrc || DataImage.Info; THREE.Sprite.call( this ); this.type = 'infospot'; this.animated = animated !== undefined ? animated : true; this.isHovering = false; /* * TODO: Three.js bug hotfix for sprite raycasting r104 * https://github.com/mrdoob/three.js/issues/14624 */ this.frustumCulled = false; this.element = null; this.toPanorama = null; this.cursorStyle = null; this.mode = MODES.NORMAL; this.scale.set( scale, scale, 1 ); this.rotation.y = Math.PI; this.container = null; this.originalRaycast = this.raycast; // Event Handler this.HANDLER_FOCUS = null; this.material.side = THREE.DoubleSide; this.material.depthTest = false; this.material.transparent = true; this.material.opacity = 0; this.scaleUpAnimation = new TWEEN.Tween(); this.scaleDownAnimation = new TWEEN.Tween(); const postLoad = function ( texture ) { if ( !this.material ) { return; } const ratio = texture.image.width / texture.image.height; const textureScale = new THREE.Vector3(); texture.image.width = texture.image.naturalWidth || 64; texture.image.height = texture.image.naturalHeight || 64; this.scale.set( ratio * scale, scale, 1 ); textureScale.copy( this.scale ); this.scaleUpAnimation = new TWEEN.Tween( this.scale ) .to( { x: textureScale.x * scaleFactor, y: textureScale.y * scaleFactor }, duration ) .easing( TWEEN.Easing.Elastic.Out ); this.scaleDownAnimation = new TWEEN.Tween( this.scale ) .to( { x: textureScale.x, y: textureScale.y }, duration ) .easing( TWEEN.Easing.Elastic.Out ); this.material.map = texture; this.material.needsUpdate = true; }.bind( this ); // Add show and hide animations this.showAnimation = new TWEEN.Tween( this.material ) .to( { opacity: 1 }, duration ) .onStart( this.enableRaycast.bind( this, true ) ) .easing( TWEEN.Easing.Quartic.Out ); this.hideAnimation = new TWEEN.Tween( this.material ) .to( { opacity: 0 }, duration ) .onStart( this.enableRaycast.bind( this, false ) ) .easing( TWEEN.Easing.Quartic.Out ); // Attach event listeners this.addEventListener( 'click', this.onClick ); this.addEventListener( 'hover', this.onHover ); this.addEventListener( 'hoverenter', this.onHoverStart ); this.addEventListener( 'hoverleave', this.onHoverEnd ); this.addEventListener( 'panolens-dual-eye-effect', this.onDualEyeEffect ); this.addEventListener( 'panolens-container', this.setContainer.bind( this ) ); this.addEventListener( 'dismiss', this.onDismiss ); this.addEventListener( 'panolens-infospot-focus', this.setFocusMethod ); TextureLoader.load( imageSrc, postLoad ); }; Infospot.prototype = Object.assign( Object.create( THREE.Sprite.prototype ), { constructor: Infospot, /** * Set infospot container * @param {HTMLElement|object} data - Data with container information * @memberOf Infospot * @instance */ setContainer: function ( data ) { let container; if ( data instanceof HTMLElement ) { container = data; } else if ( data && data.container ) { container = data.container; } // Append element if exists if ( container && this.element ) { container.appendChild( this.element ); } this.container = container; }, /** * Get container * @memberOf Infospot * @instance * @return {HTMLElement} - The container of this infospot */ getContainer: function () { return this.container; }, /** * This will be called by a click event * Translate and lock the hovering element if any * @param {object} event - Event containing mouseEvent with clientX and clientY * @memberOf Infospot * @instance */ onClick: function ( event ) { if ( this.element && this.getContainer() ) { this.onHoverStart( event ); // Lock element this.lockHoverElement(); } }, /** * Dismiss current element if any * @param {object} event - Dismiss event * @memberOf Infospot * @instance */ onDismiss: function () { if ( this.element ) { this.unlockHoverElement(); this.onHoverEnd(); } }, /** * This will be called by a mouse hover event * Translate the hovering element if any * @param {object} event - Event containing mouseEvent with clientX and clientY * @memberOf Infospot * @instance */ onHover: function () {}, /** * This will be called on a mouse hover start * Sets cursor style to 'pointer', display the element and scale up the infospot * @param {object} event * @memberOf Infospot * @instance */ onHoverStart: function ( event ) { if ( !this.getContainer() ) { return; } const cursorStyle = this.cursorStyle || ( this.mode === MODES.NORMAL ? 'pointer' : 'default' ); const { scaleDownAnimation, scaleUpAnimation, element } = this; this.isHovering = true; this.container.style.cursor = cursorStyle; if ( this.animated ) { scaleDownAnimation.stop(); scaleUpAnimation.start(); } if ( element && event.mouseEvent.clientX >= 0 && event.mouseEvent.clientY >= 0 ) { const { left, right, style } = element; if ( this.mode === MODES.CARDBOARD || this.mode === MODES.STEREO ) { style.display = 'none'; left.style.display = 'block'; right.style.display = 'block'; // Store element width for reference element._width = left.clientWidth; element._height = left.clientHeight; } else { style.display = 'block'; if ( left ) { left.style.display = 'none'; } if ( right ) { right.style.display = 'none'; } // Store element width for reference element._width = element.clientWidth; element._height = element.clientHeight; } } }, /** * This will be called on a mouse hover end * Sets cursor style to 'default', hide the element and scale down the infospot * @memberOf Infospot * @instance */ onHoverEnd: function () { if ( !this.getContainer() ) { return; } const { scaleDownAnimation, scaleUpAnimation, element } = this; this.isHovering = false; this.container.style.cursor = 'default'; if ( this.animated ) { scaleUpAnimation.stop(); scaleDownAnimation.start(); } if ( element && !this.element.locked ) { const { left, right, style } = element; style.display = 'none'; if ( left ) { left.style.display = 'none'; } if ( right ) { right.style.display = 'none'; } this.unlockHoverElement(); } }, /** * On dual eye effect handler * Creates duplicate left and right element * @param {object} event - panolens-dual-eye-effect event * @memberOf Infospot * @instance */ onDualEyeEffect: function ( event ) { if ( !this.getContainer() ) { return; } let element, halfWidth, halfHeight; this.mode = event.mode; element = this.element; halfWidth = this.container.clientWidth / 2; halfHeight = this.container.clientHeight / 2; if ( !element ) { return; } if ( !element.left && !element.right ) { element.left = element.cloneNode( true ); element.right = element.cloneNode( true ); } if ( this.mode === MODES.CARDBOARD || this.mode === MODES.STEREO ) { element.left.style.display = element.style.display; element.right.style.display = element.style.display; element.style.display = 'none'; } else { element.style.display = element.left.style.display; element.left.style.display = 'none'; element.right.style.display = 'none'; } // Update elements translation this.translateElement( halfWidth, halfHeight ); this.container.appendChild( element.left ); this.container.appendChild( element.right ); }, /** * Translate the hovering element by css transform * @param {number} x - X position on the window screen * @param {number} y - Y position on the window screen * @memberOf Infospot * @instance */ translateElement: function ( x, y ) { if ( !this.element._width || !this.element._height || !this.getContainer() ) { return; } let left, top, element, width, height, delta, container; container = this.container; element = this.element; width = element._width / 2; height = element._height / 2; delta = element.verticalDelta !== undefined ? element.verticalDelta : 40; left = x - width; top = y - height - delta; if ( ( this.mode === MODES.CARDBOARD || this.mode === MODES.STEREO ) && element.left && element.right && !( x === container.clientWidth / 2 && y === container.clientHeight / 2 ) ) { left = container.clientWidth / 4 - width + ( x - container.clientWidth / 2 ); top = container.clientHeight / 2 - height - delta + ( y - container.clientHeight / 2 ); this.setElementStyle( 'transform', element.left, 'translate(' + left + 'px, ' + top + 'px)' ); left += container.clientWidth / 2; this.setElementStyle( 'transform', element.right, 'translate(' + left + 'px, ' + top + 'px)' ); } else { this.setElementStyle( 'transform', element, 'translate(' + left + 'px, ' + top + 'px)' ); } }, /** * Set vendor specific css * @param {string} type - CSS style name * @param {HTMLElement} element - The element to be modified * @param {string} value - Style value * @memberOf Infospot * @instance */ setElementStyle: function ( type, element, value ) { const style = element.style; if ( type === 'transform' ) { style.webkitTransform = style.msTransform = style.transform = value; } }, /** * Set hovering text content * @param {string} text - Text to be displayed * @memberOf Infospot * @instance */ setText: function ( text ) { if ( this.element ) { this.element.textContent = text; } }, /** * Set cursor css style on hover * @memberOf Infospot * @instance */ setCursorHoverStyle: function ( style ) { this.cursorStyle = style; }, /** * Add hovering text element * @param {string} text - Text to be displayed * @param {number} [delta=40] - Vertical delta to the infospot * @memberOf Infospot * @instance */ addHoverText: function ( text, delta = 40 ) { if ( !this.element ) { this.element = document.createElement( 'div' ); this.element.style.display = 'none'; this.element.style.color = '#fff'; this.element.style.top = 0; this.element.style.maxWidth = '50%'; this.element.style.maxHeight = '50%'; this.element.style.textShadow = '0 0 3px #000000'; this.element.style.fontFamily = '"Trebuchet MS", Helvetica, sans-serif'; this.element.style.position = 'absolute'; this.element.classList.add( 'panolens-infospot' ); this.element.verticalDelta = delta; } this.setText( text ); }, /** * Add hovering element by cloning an element * @param {HTMLDOMElement} el - Element to be cloned and displayed * @param {number} [delta=40] - Vertical delta to the infospot * @memberOf Infospot * @instance */ addHoverElement: function ( el, delta = 40 ) { if ( !this.element ) { this.element = el.cloneNode( true ); this.element.style.display = 'none'; this.element.style.top = 0; this.element.style.position = 'absolute'; this.element.classList.add( 'panolens-infospot' ); this.element.verticalDelta = delta; } }, /** * Remove hovering element * @memberOf Infospot * @instance */ removeHoverElement: function () { if ( this.element ) { if ( this.element.left ) { this.container.removeChild( this.element.left ); this.element.left = null; } if ( this.element.right ) { this.container.removeChild( this.element.right ); this.element.right = null; } this.container.removeChild( this.element ); this.element = null; } }, /** * Lock hovering element * @memberOf Infospot * @instance */ lockHoverElement: function () { if ( this.element ) { this.element.locked = true; } }, /** * Unlock hovering element * @memberOf Infospot * @instance */ unlockHoverElement: function () { if ( this.element ) { this.element.locked = false; } }, /** * Enable raycasting * @param {boolean} [enabled=true] * @memberOf Infospot * @instance */ enableRaycast: function ( enabled = true ) { if ( enabled ) { this.raycast = this.originalRaycast; } else { this.raycast = () => {}; } }, /** * Show infospot * @param {number} [delay=0] - Delay time to show * @memberOf Infospot * @instance */ show: function ( delay = 0 ) { const { animated, hideAnimation, showAnimation, material } = this; if ( animated ) { hideAnimation.stop(); showAnimation.delay( delay ).start(); } else { this.enableRaycast( true ); material.opacity = 1; } }, /** * Hide infospot * @param {number} [delay=0] - Delay time to hide * @memberOf Infospot * @instance */ hide: function ( delay = 0 ) { const { animated, hideAnimation, showAnimation, material, element } = this; if ( element ) { const { style } = element; style.display = 'none'; } if ( animated ) { showAnimation.stop(); hideAnimation.delay( delay ).start(); } else { this.enableRaycast( false ); material.opacity = 0; } }, /** * Set focus event handler * @memberOf Infospot * @instance */ setFocusMethod: function ( event ) { if ( event ) { this.HANDLER_FOCUS = event.method; } }, /** * Focus camera center to this infospot * @param {number} [duration=1000] - Duration to tween * @param {function} [easing=TWEEN.Easing.Exponential.Out] - Easing function * @memberOf Infospot * @instance */ focus: function ( duration, easing ) { if ( this.HANDLER_FOCUS ) { this.HANDLER_FOCUS( this.position, duration, easing ); this.onDismiss(); } }, /** * Dispose * @memberOf Infospot * @instance */ dispose: function () { const { geometry, material } = this; const { map } = material; this.removeHoverElement(); if ( this.parent ) { this.parent.remove( this ); } if ( map ) { map.dispose(); material.map = null; } if ( geometry ) { geometry.dispose(); this.geometry = null; } if ( material ) { material.dispose(); this.material = null; } } } ); export { Infospot };