photo-sphere-viewer
Version:
A JavaScript library to display Photo Sphere panoramas
312 lines (261 loc) • 7.98 kB
JavaScript
import { Object3D, Vector3 } from 'three';
import { AbstractPlugin, CONSTANTS, DEFAULTS, registerButton, utils } from '../..';
import { EVENTS } from './constants';
import { DeviceOrientationControls } from './DeviceOrientationControls';
import { GyroscopeButton } from './GyroscopeButton';
/**
* @typedef {Object} PSV.plugins.GyroscopePlugin.Options
* @property {boolean} [touchmove=true] - allows to pan horizontally when the gyroscope is enabled (requires global `mousemove=true`)
* @property {boolean} [absolutePosition=false] - when true the view will ignore the current direction when enabling gyroscope control
* @property {'smooth' | 'fast'} [moveMode='smooth'] - How the gyroscope data is used to rotate the panorama.
*/
// add gyroscope button
DEFAULTS.lang[GyroscopeButton.id] = 'Gyroscope';
registerButton(GyroscopeButton, 'caption:right');
export { EVENTS } from './constants';
const direction = new Vector3();
/**
* @summary Adds gyroscope controls on mobile devices
* @extends PSV.plugins.AbstractPlugin
* @memberof PSV.plugins
*/
export class GyroscopePlugin extends AbstractPlugin {
static id = 'gyroscope';
static EVENTS = EVENTS;
/**
* @param {PSV.Viewer} psv
* @param {PSV.plugins.GyroscopePlugin.Options} options
*/
constructor(psv, options) {
super(psv);
/**
* @member {Object}
* @private
* @property {Promise<boolean>} isSupported - indicates of the gyroscope API is available
* @property {number} alphaOffset - current alpha offset for gyroscope controls
* @property {boolean} enabled
* @property {boolean} config_moveInertia - original config "moveInertia"
*/
this.prop = {
isSupported : this.__checkSupport(),
alphaOffset : 0,
enabled : false,
config_moveInertia: true,
};
/**
* @member {PSV.plugins.GyroscopePlugin.Options}
* @private
*/
this.config = {
touchmove : true,
absolutePosition: false,
moveMode: 'smooth',
...options,
};
/**
* @member {DeviceOrientationControls}
* @private
*/
this.controls = null;
}
/**
* @package
*/
init() {
super.init();
this.psv.on(CONSTANTS.EVENTS.STOP_ALL, this);
this.psv.on(CONSTANTS.EVENTS.BEFORE_ROTATE, this);
this.psv.on(CONSTANTS.EVENTS.BEFORE_RENDER, this);
}
/**
* @package
*/
destroy() {
this.psv.off(CONSTANTS.EVENTS.STOP_ALL, this);
this.psv.off(CONSTANTS.EVENTS.BEFORE_ROTATE, this);
this.psv.off(CONSTANTS.EVENTS.BEFORE_RENDER, this);
this.stop();
delete this.controls;
super.destroy();
}
/**
* @private
*/
handleEvent(e) {
switch (e.type) {
case CONSTANTS.EVENTS.STOP_ALL:
this.stop();
break;
case CONSTANTS.EVENTS.BEFORE_RENDER:
this.__onBeforeRender();
break;
case CONSTANTS.EVENTS.BEFORE_ROTATE:
this.__onBeforeRotate(e);
break;
default:
break;
}
}
/**
* @summary Checks if the gyroscope is enabled
* @returns {boolean}
*/
isEnabled() {
return this.prop.enabled;
}
/**
* @summary Enables the gyroscope navigation if available
* @returns {Promise}
* @fires PSV.plugins.GyroscopePlugin.gyroscope-updated
* @throws {PSV.PSVError} if the gyroscope API is not available/granted
*/
start() {
return this.prop.isSupported
.then((supported) => {
if (supported) {
return this.__requestPermission();
}
else {
utils.logWarn('gyroscope not available');
return Promise.reject();
}
})
.then((granted) => {
if (granted) {
return Promise.resolve();
}
else {
utils.logWarn('gyroscope not allowed');
return Promise.reject();
}
})
.then(() => {
this.psv.__stopAll();
// disable inertia
this.prop.config_moveInertia = this.psv.config.moveInertia;
this.psv.config.moveInertia = false;
// enable gyro controls
if (!this.controls) {
this.controls = new DeviceOrientationControls(new Object3D());
}
else {
this.controls.connect();
}
// force reset
this.controls.deviceOrientation = null;
this.controls.screenOrientation = 0;
this.controls.alphaOffset = 0;
this.prop.alphaOffset = this.config.absolutePosition ? 0 : null;
this.prop.enabled = true;
this.trigger(EVENTS.GYROSCOPE_UPDATED, true);
});
}
/**
* @summary Disables the gyroscope navigation
* @fires PSV.plugins.GyroscopePlugin.gyroscope-updated
*/
stop() {
if (this.isEnabled()) {
this.controls.disconnect();
this.prop.enabled = false;
this.psv.config.moveInertia = this.prop.config_moveInertia;
this.trigger(EVENTS.GYROSCOPE_UPDATED, false);
this.psv.resetIdleTimer();
}
}
/**
* @summary Enables or disables the gyroscope navigation
*/
toggle() {
if (this.isEnabled()) {
this.stop();
}
else {
this.start();
}
}
/**
* @summary Handles gyro movements
* @private
*/
__onBeforeRender() {
if (!this.isEnabled()) {
return;
}
if (!this.controls.deviceOrientation) {
return;
}
const position = this.psv.getPosition();
// on first run compute the offset depending on the current viewer position and device orientation
if (this.prop.alphaOffset === null) {
this.controls.update();
this.controls.object.getWorldDirection(direction);
const sphericalCoords = this.psv.dataHelper.vector3ToSphericalCoords(direction);
this.prop.alphaOffset = sphericalCoords.longitude - position.longitude;
}
else {
this.controls.alphaOffset = this.prop.alphaOffset;
this.controls.update();
this.controls.object.getWorldDirection(direction);
const sphericalCoords = this.psv.dataHelper.vector3ToSphericalCoords(direction);
const target = {
longitude: sphericalCoords.longitude,
latitude : -sphericalCoords.latitude,
};
// having a slow speed on smalls movements allows to absorb the device/hand vibrations
const step = this.config.moveMode === 'smooth' ? 3 : 10;
this.psv.dynamics.position.goto(target, utils.getAngle(position, target) < 0.01 ? 1 : step);
}
}
/**
* @summary Intercepts moves and offsets the alpha angle
* @param {external:uEvent.Event} e
* @private
*/
__onBeforeRotate(e) {
if (this.isEnabled()) {
e.preventDefault();
if (this.config.touchmove) {
this.prop.alphaOffset -= e.args[0].longitude - this.psv.getPosition().longitude;
}
}
}
/**
* @summary Detects if device orientation is supported
* @returns {Promise<boolean>}
* @private
*/
__checkSupport() {
if ('DeviceMotionEvent' in window && typeof DeviceMotionEvent.requestPermission === 'function') {
return Promise.resolve(true);
}
else if ('DeviceOrientationEvent' in window) {
return new Promise((resolve) => {
const listener = (e) => {
resolve(e && e.alpha !== null && !isNaN(e.alpha));
window.removeEventListener('deviceorientation', listener);
};
window.addEventListener('deviceorientation', listener, false);
setTimeout(listener, 10000);
});
}
else {
return Promise.resolve(false);
}
}
/**
* @summary Request permission to the motion API
* @returns {Promise<boolean>}
* @private
*/
__requestPermission() {
if ('DeviceMotionEvent' in window && typeof DeviceMotionEvent.requestPermission === 'function') {
return DeviceOrientationEvent.requestPermission()
.then(response => response === 'granted')
.catch(() => false);
}
else {
return Promise.resolve(true);
}
}
}