photo-sphere-viewer
Version:
A JavaScript library to display Photo Sphere panoramas
420 lines (360 loc) • 11.3 kB
JavaScript
import { Group, PerspectiveCamera, Raycaster, Scene, Vector3, WebGLRenderer } from 'three';
import { EVENTS, MESH_USER_DATA, SPHERE_RADIUS } from '../data/constants';
import { SYSTEM } from '../data/system';
import { Animation, each, isExtendedPosition } from '../utils';
import { AbstractService } from './AbstractService';
/**
* @summary Viewer and renderer
* @extends PSV.services.AbstractService
* @memberof PSV.services
*/
export class Renderer extends AbstractService {
/**
* @param {PSV.Viewer} psv
*/
constructor(psv) {
super(psv);
/**
* @member {external:THREE.WebGLRenderer}
* @readonly
* @protected
*/
this.renderer = new WebGLRenderer({ alpha: true, antialias: true });
this.renderer.setPixelRatio(SYSTEM.pixelRatio);
this.renderer.domElement.className = 'psv-canvas';
/**
* @member {external:THREE.Scene}
* @readonly
* @protected
*/
this.scene = new Scene();
/**
* @member {external:THREE.PerspectiveCamera}
* @readonly
* @protected
*/
this.camera = new PerspectiveCamera(50, 16 / 9, 0.1, 2 * SPHERE_RADIUS);
/**
* @member {external:THREE.Mesh}
* @readonly
* @protected
*/
this.mesh = this.psv.adapter.createMesh();
this.mesh.userData = { [MESH_USER_DATA]: true };
/**
* @member {external:THREE.Group}
* @readonly
* @private
*/
this.meshContainer = new Group();
this.meshContainer.add(this.mesh);
this.scene.add(this.meshContainer);
/**
* @member {external:THREE.Raycaster}
* @readonly
* @protected
*/
this.raycaster = new Raycaster();
/**
* @member {number}
* @private
*/
this.timestamp = null;
/**
* @member {boolean}
* @private
*/
this.ready = false;
/**
* @member {HTMLElement}
* @readonly
* @package
*/
this.canvasContainer = document.createElement('div');
this.canvasContainer.className = 'psv-canvas-container';
this.canvasContainer.style.background = this.psv.config.canvasBackground;
this.canvasContainer.style.cursor = this.psv.config.mousemove ? 'move' : 'default';
this.canvasContainer.appendChild(this.renderer.domElement);
this.psv.container.appendChild(this.canvasContainer);
psv.on(EVENTS.SIZE_UPDATED, this);
psv.on(EVENTS.ZOOM_UPDATED, this);
psv.on(EVENTS.POSITION_UPDATED, this);
psv.on(EVENTS.CONFIG_CHANGED, this);
this.hide();
}
/**
* @override
*/
destroy() {
// cancel render loop
this.renderer.setAnimationLoop(null);
// destroy ThreeJS view
this.__cleanTHREEScene(this.scene);
// remove container
this.psv.container.removeChild(this.canvasContainer);
delete this.canvasContainer;
delete this.renderer;
delete this.scene;
delete this.camera;
delete this.mesh;
delete this.meshContainer;
delete this.raycaster;
super.destroy();
}
/**
* @summary Handles events
* @param {Event} evt
* @private
*/
handleEvent(evt) {
/* eslint-disable */
switch (evt.type) {
// @formatter:off
case EVENTS.SIZE_UPDATED: this.__onSizeUpdated(); break;
case EVENTS.ZOOM_UPDATED: this.__onZoomUpdated(); break;
case EVENTS.POSITION_UPDATED: this.__onPositionUpdated(); break;
case EVENTS.CONFIG_CHANGED:
if (evt.args[0].includes('fisheye')) {
this.__onPositionUpdated();
}
if (evt.args[0].includes('mousemove')) {
this.canvasContainer.style.cursor = this.psv.config.mousemove ? 'move' : 'default';
}
break;
// @formatter:on
}
/* eslint-enable */
}
/**
* @summary Hides the viewer
*/
hide() {
this.canvasContainer.style.opacity = 0;
}
/**
* @summary Shows the viewer
*/
show() {
this.canvasContainer.style.opacity = 1;
}
/**
* @summary Updates the size of the renderer and the aspect of the camera
* @private
*/
__onSizeUpdated() {
this.renderer.setSize(this.prop.size.width, this.prop.size.height);
this.camera.aspect = this.prop.aspect;
this.camera.updateProjectionMatrix();
this.prop.needsUpdate = true;
}
/**
* @summary Updates the fov of the camera
* @private
*/
__onZoomUpdated() {
this.camera.fov = this.prop.vFov;
this.camera.updateProjectionMatrix();
this.prop.needsUpdate = true;
}
/**
* @summary Updates the position of the camera
* @private
*/
__onPositionUpdated() {
this.camera.position.set(0, 0, 0);
this.camera.lookAt(this.prop.direction);
if (this.config.fisheye) {
this.camera.position.copy(this.prop.direction).multiplyScalar(this.config.fisheye / 2).negate();
}
this.prop.needsUpdate = true;
}
/**
* @summary Main event loop, calls {@link render} if `prop.needsUpdate` is true
* @param {number} timestamp
* @fires PSV.before-render
* @private
*/
__renderLoop(timestamp) {
const elapsed = this.timestamp !== null ? timestamp - this.timestamp : 0;
this.timestamp = timestamp;
this.psv.trigger(EVENTS.BEFORE_RENDER, timestamp, elapsed);
each(this.psv.dynamics, d => d.update(elapsed));
if (this.prop.idleTime > 0 && timestamp - this.prop.idleTime > this.config.autorotateDelay) {
this.psv.startAutorotate();
}
if (this.prop.needsUpdate) {
this.render();
this.prop.needsUpdate = false;
}
}
/**
* @summary Performs a render
* @description Do not call this method directly, instead call
* {@link PSV.Viewer#needsUpdate} on {@link PSV.event:before-render}.
* @fires PSV.render
*/
render() {
this.renderer.render(this.scene, this.camera);
this.psv.trigger(EVENTS.RENDER);
}
/**
* @summary Applies the texture to the scene, creates the scene if needed
* @param {PSV.TextureData} textureData
* @fires PSV.panorama-loaded
* @package
*/
setTexture(textureData) {
this.prop.panoData = textureData.panoData;
this.psv.adapter.setTexture(this.mesh, textureData);
if (!this.ready) {
this.renderer.setAnimationLoop(t => this.__renderLoop(t));
this.ready = true;
}
this.psv.needsUpdate();
this.psv.trigger(EVENTS.PANORAMA_LOADED, textureData);
}
/**
* @summary Applies the overlay to the mesh
* @param {PSV.TextureData} textureData
* @param {number} opacity
* @package
*/
setOverlay(textureData, opacity) {
this.psv.adapter.setOverlay(this.mesh, textureData, opacity);
this.psv.needsUpdate();
}
/**
* @summary Apply a panorama data pose to a Mesh
* @param {PSV.PanoData} [panoData]
* @param {external:THREE.Mesh} [mesh=this.mesh]
* @package
*/
setPanoramaPose(panoData, mesh = this.mesh) {
// By Google documentation the angles are applied on the camera in order : heading, pitch, roll
// here we apply the reverse transformation on the sphere
const cleanCorrection = this.psv.dataHelper.cleanPanoramaPose(panoData);
mesh.rotation.set(
-cleanCorrection.tilt,
-cleanCorrection.pan,
-cleanCorrection.roll,
'ZXY'
);
}
/**
* @summary Apply a SphereCorrection to a Mesh
* @param {PSV.SphereCorrection} [sphereCorrection]
* @param {external:THREE.Mesh} [mesh=this.meshContainer]
* @package
*/
setSphereCorrection(sphereCorrection, mesh = this.meshContainer) {
const cleanCorrection = this.psv.dataHelper.cleanSphereCorrection(sphereCorrection);
mesh.rotation.set(
cleanCorrection.tilt,
cleanCorrection.pan,
cleanCorrection.roll,
'ZXY'
);
}
/**
* @summary Performs transition between the current and a new texture
* @param {PSV.TextureData} textureData
* @param {PSV.PanoramaOptions} options
* @returns {PSV.utils.Animation}
* @package
*/
transition(textureData, options) {
const positionProvided = isExtendedPosition(options);
const zoomProvided = 'zoom' in options;
// create temp group and new mesh, half size to be in "front" of the first one
const group = new Group();
const mesh = this.psv.adapter.createMesh(0.5);
this.psv.adapter.setTexture(mesh, textureData, true);
this.psv.adapter.setTextureOpacity(mesh, 0);
this.setPanoramaPose(textureData.panoData, mesh);
this.setSphereCorrection(options.sphereCorrection, group);
// rotate the new sphere to make the target position face the camera
if (positionProvided) {
const cleanPosition = this.psv.dataHelper.cleanPosition(options);
const currentPosition = this.psv.getPosition();
// Longitude rotation along the vertical axis
const verticalAxis = new Vector3(0, 1, 0);
group.rotateOnWorldAxis(verticalAxis, cleanPosition.longitude - currentPosition.longitude);
// Latitude rotation along the camera horizontal axis
const horizontalAxis = new Vector3(0, 1, 0).cross(this.camera.getWorldDirection(new Vector3())).normalize();
group.rotateOnWorldAxis(horizontalAxis, cleanPosition.latitude - currentPosition.latitude);
}
group.add(mesh);
this.scene.add(group);
const animation = new Animation({
properties: {
opacity: { start: 0.0, end: 1.0 },
zoom : zoomProvided ? { start: this.psv.getZoomLevel(), end: options.zoom } : undefined,
},
duration : options.transition,
easing : 'outCubic',
onTick : (properties) => {
this.psv.adapter.setTextureOpacity(mesh, properties.opacity);
this.psv.adapter.setTextureOpacity(this.mesh, 1 - properties.opacity);
if (zoomProvided) {
this.psv.zoom(properties.zoom);
}
this.psv.needsUpdate();
},
});
animation
.then((completed) => {
if (completed) {
// remove temp sphere and transfer the texture to the main mesh
this.setTexture(textureData);
this.psv.adapter.setTextureOpacity(this.mesh, 1);
this.setPanoramaPose(textureData.panoData);
this.setSphereCorrection(options.sphereCorrection);
// actually rotate the camera
if (positionProvided) {
this.psv.rotate(options);
}
}
else {
this.psv.adapter.disposeTexture(textureData);
}
this.scene.remove(group);
mesh.geometry.dispose();
mesh.geometry = null;
});
return animation;
}
/**
* @summary Calls `dispose` on all objects and textures
* @param {external:THREE.Object3D} object
* @private
*/
__cleanTHREEScene(object) {
object.traverse((item) => {
if (item.geometry) {
item.geometry.dispose();
}
if (item.material) {
if (Array.isArray(item.material)) {
item.material.forEach((material) => {
if (material.map) {
material.map.dispose();
}
material.dispose();
});
}
else {
if (item.material.map) {
item.material.map.dispose();
}
item.material.dispose();
}
}
if (item.dispose && !(item instanceof Scene)) {
item.dispose();
}
if (item !== object) {
this.__cleanTHREEScene(item);
}
});
}
}