From 9fe015a7bdf29db70d605bf4c8fe1d34801c96dd Mon Sep 17 00:00:00 2001 From: mistic100 Date: Mon, 5 Apr 2021 15:48:29 +0200 Subject: [PATCH] Close #526 Add move buttons, rework move and zoom --- .eslintrc.json | 7 +- docs/.vuepress/components/Playground.vue | 12 +- docs/guide/config.md | 14 +- docs/guide/migration-v3.md | 3 +- docs/guide/navbar.md | 32 ++-- example/cubemap.html | 2 +- example/equirectangular.html | 4 +- example/plugin-autorotate-keypoints.html | 1 - example/plugin-markers.html | 4 +- example/plugin-resolution.html | 1 + src/Viewer.js | 179 ++++++++++---------- src/ViewerCompat.js | 12 +- src/buttons/AbstractMoveButton.js | 117 +++++++++++++ src/buttons/AbstractZoomButton.js | 61 +------ src/buttons/MoveDownButton.js | 20 +++ src/buttons/MoveLeftButton.js | 20 +++ src/buttons/MoveRightButton.js | 20 +++ src/buttons/MoveUpButton.js | 20 +++ src/buttons/ZoomInButton.js | 2 +- src/buttons/ZoomOutButton.js | 2 +- src/buttons/ZoomRangeButton.js | 4 +- src/components/Navbar.js | 14 ++ src/components/Notification.js | 2 +- src/components/Overlay.js | 2 +- src/components/Panel.js | 2 +- src/data/config.js | 24 ++- src/data/constants.js | 1 + src/icons/arrow.svg | 1 + src/plugins/autorotate-keypoints/index.js | 33 ++-- src/plugins/gyroscope/compass.svg | 2 +- src/plugins/gyroscope/index.js | 106 ++++++------ src/plugins/markers/index.js | 2 +- src/plugins/visible-range/index.js | 14 +- src/services/DataHelper.js | 35 ++-- src/services/EventsHandler.js | 89 +++++----- src/services/Renderer.js | 30 ++-- src/services/TextureLoader.js | 1 - src/styles/navbar.scss | 4 + src/utils/Dynamic.js | 191 ++++++++++++++++++++++ src/utils/DynamicXD.js | 191 ++++++++++++++++++++++ src/utils/MultiDynamic.js | 125 ++++++++++++++ src/utils/PressHandler.js | 43 +++++ src/utils/PressHandler.spec.js | 32 ++++ 43 files changed, 1140 insertions(+), 341 deletions(-) create mode 100644 src/buttons/AbstractMoveButton.js create mode 100644 src/buttons/MoveDownButton.js create mode 100644 src/buttons/MoveLeftButton.js create mode 100644 src/buttons/MoveRightButton.js create mode 100644 src/buttons/MoveUpButton.js create mode 100644 src/icons/arrow.svg create mode 100644 src/utils/Dynamic.js create mode 100644 src/utils/DynamicXD.js create mode 100644 src/utils/MultiDynamic.js create mode 100644 src/utils/PressHandler.js create mode 100644 src/utils/PressHandler.spec.js diff --git a/.eslintrc.json b/.eslintrc.json index 579ae9f09..c40211ee1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,7 @@ "no-bitwise": "off", "no-else-return": "off", "no-multiple-empty-lines": "off", + "no-param-reassign": "off", "no-plusplus": "off", "no-restricted-properties": "off", "no-underscore-dangle": "off", @@ -55,12 +56,6 @@ "switches": "never" } ], - "no-param-reassign": [ - "error", - { - "props": false - } - ], "no-console": [ "error", { diff --git a/docs/.vuepress/components/Playground.vue b/docs/.vuepress/components/Playground.vue index 7f7cb3b23..1b12bf2a7 100644 --- a/docs/.vuepress/components/Playground.vue +++ b/docs/.vuepress/components/Playground.vue @@ -178,20 +178,14 @@
- +
- - - -
-
- - - + +
diff --git a/docs/guide/config.md b/docs/guide/config.md index 8744cf78a..f6988809a 100644 --- a/docs/guide/config.md +++ b/docs/guide/config.md @@ -218,19 +218,13 @@ In a future version the order in which the angles are applied will change. It is - type: `double` - default `1` -Speed multiplicator for manual moves. +Speed multiplicator for panorama moves. Used for click move, touch move and navbar buttons. -#### `zoomButtonIncrement` +#### `zoomSpeed` - type: `double` -- default `2` - -Zoom increment when using the keyboard or the navbar buttons. - -#### `mousewheelSpeed` -- type: `double` -- default: `1` +- default `1` -Zoom speed when using the mouse wheel. +Speed multiplicator for panorama zooms. Used for mouse wheel, touch pinch and navbar buttons. #### `useXmpData` - type: `boolean` diff --git a/docs/guide/migration-v3.md b/docs/guide/migration-v3.md index 3ce9a16c4..6cd083595 100644 --- a/docs/guide/migration-v3.md +++ b/docs/guide/migration-v3.md @@ -59,8 +59,6 @@ Be sure to rename your configuration properties, the old naming is not supported - `time_anim` → `autorotateDelay` - `default_fov`→ `defaultZoomLvl` - `mousemove_hover` → `captureCursor` -- `zoom_speed` → `zoomButtonIncrement` -- `mousewheel_factor` → `mousewheelSpeed` ### Deleted options @@ -68,6 +66,7 @@ Be sure to rename your configuration properties, the old naming is not supported - `tooltip` → the properties of the tooltip are now extracted from the stylesheet - `webgl` → WebGL is now always enabled since three.js deprecated the CanvasRenderer - `panorama_roll` → use `sphereCorrection` with the `roll` property +- `mousewheel_factor` ## Methods diff --git a/docs/guide/navbar.md b/docs/guide/navbar.md index 7874bbe9d..babb2156a 100644 --- a/docs/guide/navbar.md +++ b/docs/guide/navbar.md @@ -6,19 +6,25 @@ The `navbar` option is an array which can contain the following elements: - - `autorotate` : toggles the automatic rotation - - `zoomOut` : zooms in - - `zoomRange` : zoom slider - - `zoomIn` : zooms out - - `zoom`: `zoomOut` + `zoomRange` + `zoomIn` - - `download` : download the source image - - `caption` : the caption - - `fullscreen` : toggles fullscreen view - - ~~`markers` : toggles the markers~~ now part of a [plugin](../plugins/plugin-markers.md) - - ~~`markersList` : shows the markers list~~ now part of a [plugin](../plugins/plugin-markers.md) - - ~~`gyroscope` : toggles the gyroscope~~ now part of a [plugin](../plugins/plugin-gyroscope.md) - - ~~`stereo` : toggles stereo view (VR)~~ now part of a [plugin](../plugins/plugin-stereo.md) - + - `autorotate` + - `zoomOut` + - `zoomRange` + - `zoomIn` + - `zoom` = `zoomOut` + `zoomRange` + `zoomIn` + - `moveLeft` + - `moveRight` + - `moveTop` + - `moveDown` + - `move` = `moveLeft` + `moveRight` + `moveTop` + `moveDown` + - `download` + - `caption` + - `fullscreen` + - ~~`markers`~~ now part of a [plugin](../plugins/plugin-markers.md) + - ~~`markersList`~~ now part of a [plugin](../plugins/plugin-markers.md) + - ~~`gyroscope`~~ now part of a [plugin](../plugins/plugin-gyroscope.md) + - ~~`stereo`~~ now part of a [plugin](../plugins/plugin-stereo.md) + +Other buttons can be made available by plugins. ## Custom buttons diff --git a/example/cubemap.html b/example/cubemap.html index 6110e2f18..5092883cc 100644 --- a/example/cubemap.html +++ b/example/cubemap.html @@ -71,7 +71,7 @@ caption : panos[0].desc, loadingImg: 'assets/photosphere-logo.gif', navbar : [ - 'autorotate', 'zoom', 'download', 'markers', 'markersList', + 'autorotate', 'zoom', 'move', { title : 'Change image', className: 'custom-button', diff --git a/example/equirectangular.html b/example/equirectangular.html index 4e03a0a0d..548d851ae 100644 --- a/example/equirectangular.html +++ b/example/equirectangular.html @@ -60,8 +60,10 @@ touchmoveTwoFingers: false, mousewheelCtrlKey : false, captureCursor : false, + moveSpeed : 1, + zoomSpeed : 1, navbar : [ - 'autorotate', 'zoom', 'download', + 'autorotate', 'zoom', 'move', 'download', { title : 'Change image', className: 'custom-button', diff --git a/example/plugin-autorotate-keypoints.html b/example/plugin-autorotate-keypoints.html index 0994506b0..3e66dce59 100644 --- a/example/plugin-autorotate-keypoints.html +++ b/example/plugin-autorotate-keypoints.html @@ -45,7 +45,6 @@ autorotateDelay: 1000, navbar : [ 'autorotate', - 'zoom', { title : 'Change points', content: '🔄', diff --git a/example/plugin-markers.html b/example/plugin-markers.html index f716fd0dc..136acdc6f 100644 --- a/example/plugin-markers.html +++ b/example/plugin-markers.html @@ -114,7 +114,7 @@

Header Level 3

caption : 'Parc national du Mercantour © Damien Sorel', loadingImg: 'assets/photosphere-logo.gif', navbar : [ - 'autorotate', 'zoom', 'download', 'markers', 'markersList', + 'markers', 'markersList', { content : '💬', title : 'Show all tooltips', @@ -123,7 +123,7 @@

Header Level 3

markers.toggleAllTooltips(); } }, - 'caption', 'gyroscope', 'stereo', 'fullscreen', + 'caption', 'fullscreen', ], plugins : [ PhotoSphereViewer.GyroscopePlugin, diff --git a/example/plugin-resolution.html b/example/plugin-resolution.html index 8f489c3ee..ffd4dfb1b 100644 --- a/example/plugin-resolution.html +++ b/example/plugin-resolution.html @@ -40,6 +40,7 @@ panorama : 'sphere_small.jpg', caption : 'Parc national du Mercantour © Damien Sorel', loadingImg: 'assets/photosphere-logo.gif', + navbar : ['caption', 'settings', 'fullscreen'], plugins : [ PhotoSphereViewer.SettingsPlugin, [PhotoSphereViewer.ResolutionPlugin, { diff --git a/src/Viewer.js b/src/Viewer.js index 23b16f577..64ca36bb2 100644 --- a/src/Viewer.js +++ b/src/Viewer.js @@ -6,7 +6,7 @@ import { Navbar } from './components/Navbar'; import { Notification } from './components/Notification'; import { Overlay } from './components/Overlay'; import { Panel } from './components/Panel'; -import { CONFIG_PARSERS, DEFAULTS, getConfig, READONLY_OPTIONS } from './data/config'; +import { CONFIG_PARSERS, DEFAULTS, DEPRECATED_OPTIONS, getConfig, READONLY_OPTIONS } from './data/config'; import { CHANGE_EVENTS, EVENTS, IDS, VIEWER_DATA } from './data/constants'; import { SYSTEM } from './data/system'; import errorIcon from './icons/error.svg'; @@ -17,17 +17,19 @@ import { Renderer } from './services/Renderer'; import { TextureLoader } from './services/TextureLoader'; import { TooltipRenderer } from './services/TooltipRenderer'; import { - bound, each, exitFullscreen, getAngle, getShortestArc, isExtendedPosition, isFullscreenEnabled, + logWarn, requestFullscreen, throttle, toggleClass } from './utils'; +import { Dynamic } from './utils/Dynamic'; +import { MultiDynamic } from './utils/MultiDynamic'; THREE.Cache.enabled = true; @@ -64,14 +66,11 @@ export class Viewer extends EventEmitter { * @property {boolean} ready - when all components are loaded * @property {boolean} needsUpdate - if the view needs to be renderer * @property {boolean} isCubemap - if the panorama is a cubemap - * @property {PSV.Position} position - current direction of the camera * @property {external:THREE.Vector3} direction - direction of the camera - * @property {number} zoomLvl - current zoom level * @property {number} vFov - vertical FOV * @property {number} hFov - horizontal FOV * @property {number} aspect - viewer aspect ratio - * @property {number} moveSpeed - move speed (computed with pixel ratio and configuration moveSpeed) - * @property {Function} autorotateCb - update callback of the automatic rotation + * @property {boolean} autorotateEnabled - automatic rotation is enabled * @property {PSV.Animation} animationPromise - promise of the current animation (either go to position or image transition) * @property {Promise} loadingPromise - promise of the setPanorama method * @property startTimeout - timeout id of the automatic rotation delay @@ -79,30 +78,24 @@ export class Viewer extends EventEmitter { * @property {PSV.PanoData} panoData - panorama metadata */ this.prop = { - ready : false, - uiRefresh : false, - needsUpdate : false, - fullscreen : false, - isCubemap : undefined, - position : { - longitude: 0, - latitude : 0, - }, - direction : null, - zoomLvl : null, - vFov : null, - hFov : null, - aspect : null, - moveSpeed : 0.1, - autorotateCb : null, - animationPromise: null, - loadingPromise : null, - startTimeout : null, - size : { + ready : false, + uiRefresh : false, + needsUpdate : false, + fullscreen : false, + isCubemap : undefined, + direction : new THREE.Vector3(), + vFov : null, + hFov : null, + aspect : null, + autorotateEnabled: false, + animationPromise : null, + loadingPromise : null, + startTimeout : null, + size : { width : 0, height: 0, }, - panoData : { + panoData : { fullWidth : 0, fullHeight : 0, croppedWidth : 0, @@ -216,6 +209,30 @@ export class Viewer extends EventEmitter { */ this.overlay = new Overlay(this); + /** + * @member {Record} + * @package + */ + this.dynamics = { + zoom: new Dynamic((value) => { + this.prop.vFov = this.dataHelper.zoomLevelToFov(value); + this.prop.hFov = this.dataHelper.vFovToHFov(this.prop.vFov); + + this.needsUpdate(); + this.trigger(EVENTS.ZOOM_UPDATED, value); + }, 0, 100), + + position: new MultiDynamic({ + longitude: new Dynamic(null), + latitude : new Dynamic(null, -Math.PI / 2, Math.PI / 2), + }, (position) => { + this.needsUpdate(); + this.trigger(EVENTS.POSITION_UPDATED, position); + }), + }; + + this.__updateSpeeds(); + this.eventsHandler.init(); this.__resizeRefresh = throttle(() => this.refreshUi('resize'), 500); @@ -223,9 +240,6 @@ export class Viewer extends EventEmitter { // apply container size this.resize(this.config.size); - // actual move speed depends on pixel-ratio - this.prop.moveSpeed = THREE.Math.degToRad(this.config.moveSpeed / SYSTEM.pixelRatio); - // init plugins this.config.plugins.forEach(([plugin, opts]) => { this.plugins[plugin.id] = new plugin(this, opts); // eslint-disable-line new-cap @@ -345,10 +359,7 @@ export class Viewer extends EventEmitter { * @returns {PSV.Position} */ getPosition() { - return { - longitude: this.prop.position.longitude, - latitude : this.prop.position.latitude, - }; + return this.dynamics.position.current; } /** @@ -356,7 +367,7 @@ export class Viewer extends EventEmitter { * @returns {number} */ getZoomLevel() { - return this.prop.zoomLvl; + return this.dynamics.zoom.current; } /** @@ -375,7 +386,7 @@ export class Viewer extends EventEmitter { * @returns {boolean} */ isAutorotateEnabled() { - return !!this.prop.autorotateCb; + return this.prop.autorotateEnabled; } /** @@ -536,6 +547,11 @@ export class Viewer extends EventEmitter { */ setOptions(options) { each(options, (value, key) => { + if (DEPRECATED_OPTIONS[key]) { + logWarn(DEPRECATED_OPTIONS[key]); + return; + } + if (!Object.prototype.hasOwnProperty.call(DEFAULTS, key)) { throw new PSVError(`Unknown option ${key}`); } @@ -570,12 +586,13 @@ export class Viewer extends EventEmitter { break; case 'moveSpeed': - this.prop.moveSpeed = THREE.Math.degToRad(value / SYSTEM.pixelRatio); + case 'zoomSpeed': + this.__updateSpeeds(); break; case 'minFov': case 'maxFov': - this.prop.zoomLvl = this.dataHelper.fovToZoomLevel(this.prop.vFov); + this.dynamics.zoom.setValue(this.dataHelper.fovToZoomLevel(this.prop.vFov)); this.trigger(EVENTS.ZOOM_UPDATED, this.getZoomLevel()); break; @@ -611,22 +628,15 @@ export class Viewer extends EventEmitter { startAutorotate() { this.__stopAll(); - this.prop.autorotateCb = (() => { - let last; - let elapsed; + this.dynamics.position.roll({ + longitude: this.config.autorotateSpeed < 0, + }, Math.abs(this.config.autorotateSpeed / this.config.moveSpeed)); - return (e, timestamp) => { - elapsed = last === undefined ? 0 : timestamp - last; - last = timestamp; + this.dynamics.position.goto({ + latitude: this.config.autorotateLat, + }, Math.abs(this.config.autorotateSpeed / this.config.moveSpeed)); - this.rotate({ - longitude: this.prop.position.longitude + this.config.autorotateSpeed * elapsed / 1000, - latitude : this.prop.position.latitude - (this.prop.position.latitude - this.config.autorotateLat) / 200, - }); - }; - })(); - - this.on(EVENTS.BEFORE_RENDER, this.prop.autorotateCb); + this.prop.autorotateEnabled = true; this.trigger(EVENTS.AUTOROTATE, true); } @@ -642,8 +652,9 @@ export class Viewer extends EventEmitter { } if (this.isAutorotateEnabled()) { - this.off(EVENTS.BEFORE_RENDER, this.prop.autorotateCb); - this.prop.autorotateCb = null; + this.dynamics.position.stop(); + + this.prop.autorotateEnabled = false; this.trigger(EVENTS.AUTOROTATE, false); } @@ -695,15 +706,7 @@ export class Viewer extends EventEmitter { } const cleanPosition = this.change(CHANGE_EVENTS.GET_ROTATE_POSITION, this.dataHelper.cleanPosition(position)); - - if (this.prop.position.longitude !== cleanPosition.longitude || this.prop.position.latitude !== cleanPosition.latitude) { - this.prop.position.longitude = cleanPosition.longitude; - this.prop.position.latitude = cleanPosition.latitude; - - this.needsUpdate(); - - this.trigger(EVENTS.POSITION_UPDATED, this.getPosition()); - } + this.dynamics.position.setValue(cleanPosition); } /** @@ -723,21 +726,22 @@ export class Viewer extends EventEmitter { // clean/filter position and compute duration if (positionProvided) { const cleanPosition = this.change(CHANGE_EVENTS.GET_ANIMATE_POSITION, this.dataHelper.cleanPosition(options)); + const currentPosition = this.getPosition(); // longitude offset for shortest arc - const tOffset = getShortestArc(this.prop.position.longitude, cleanPosition.longitude); + const tOffset = getShortestArc(currentPosition.longitude, cleanPosition.longitude); - animProperties.longitude = { start: this.prop.position.longitude, end: this.prop.position.longitude + tOffset }; - animProperties.latitude = { start: this.prop.position.latitude, end: cleanPosition.latitude }; + animProperties.longitude = { start: currentPosition.longitude, end: currentPosition.longitude + tOffset }; + animProperties.latitude = { start: currentPosition.latitude, end: cleanPosition.latitude }; - duration = this.dataHelper.speedToDuration(options.speed, getAngle(this.prop.position, cleanPosition)); + duration = this.dataHelper.speedToDuration(options.speed, getAngle(currentPosition, cleanPosition)); } // clean/filter zoom and compute duration if (zoomProvided) { - const dZoom = Math.abs(options.zoom - this.prop.zoomLvl); + const dZoom = Math.abs(options.zoom - this.getZoomLevel()); - animProperties.zoom = { start: this.prop.zoomLvl, end: options.zoom }; + animProperties.zoom = { start: this.getZoomLevel(), end: options.zoom }; if (!duration) { // if animating zoom only and a speed is given, use an arbitrary PI/4 to compute the duration @@ -798,31 +802,23 @@ export class Viewer extends EventEmitter { * @fires PSV.zoom-updated */ zoom(level) { - const newZoomLvl = bound(level, 0, 100); - - if (this.prop.zoomLvl !== newZoomLvl) { - this.prop.zoomLvl = newZoomLvl; - this.prop.vFov = this.dataHelper.zoomLevelToFov(this.prop.zoomLvl); - this.prop.hFov = this.dataHelper.vFovToHFov(this.prop.vFov); - - this.needsUpdate(); - this.trigger(EVENTS.ZOOM_UPDATED, this.getZoomLevel()); - this.rotate(this.prop.position); - } + this.dynamics.zoom.setValue(level); } /** - * @summary Increases the zoom level by 1 + * @summary Increases the zoom level + * @param {number} [step=1] */ - zoomIn() { - this.zoom(this.prop.zoomLvl + this.config.zoomButtonIncrement); + zoomIn(step = 1) { + this.dynamics.zoom.step(step); } /** - * @summary Decreases the zoom level by 1 + * @summary Decreases the zoom level + * @param {number} [step=1] */ - zoomOut() { - this.zoom(this.prop.zoomLvl - this.config.zoomButtonIncrement); + zoomOut(step = 1) { + this.dynamics.zoom.step(-step); } /** @@ -903,7 +899,7 @@ export class Viewer extends EventEmitter { /** * @summary Stops all current animations - * @private + * @package */ __stopAll() { this.stopAutorotate(); @@ -912,4 +908,13 @@ export class Viewer extends EventEmitter { this.trigger(EVENTS.STOP_ALL); } + /** + * @summary Recomputes dynamics speeds + * @private + */ + __updateSpeeds() { + this.dynamics.zoom.setSpeed(this.config.zoomSpeed * 50); + this.dynamics.position.setSpeed(THREE.Math.degToRad(this.config.moveSpeed * 50)); + } + } diff --git a/src/ViewerCompat.js b/src/ViewerCompat.js index c580441b5..3ce79da0a 100644 --- a/src/ViewerCompat.js +++ b/src/ViewerCompat.js @@ -26,12 +26,10 @@ function snakeCaseToCamelCase(options) { * @private */ const RENAMED_OPTIONS = { - animSpeed : 'autorotateSpeed', - animLat : 'autorotateLat', - usexmpdata : 'useXmpData', - mousemoveHover : 'captureCursor', - zoomSpeed : 'zoomButtonIncrement', - mousewheelFactor: 'mousewheelSpeed', + animSpeed : 'autorotateSpeed', + animLat : 'autorotateLat', + usexmpdata : 'useXmpData', + mousemoveHover: 'captureCursor', }; /** @@ -120,6 +118,8 @@ export default class ViewerCompat extends Viewer { options.plugins.push([VisibleRangePlugin, { longitudeRange, latitudeRange }]); } + delete options.mousewheelFactor; + super(options); this.gyroscope = this.getPlugin(GyroscopePlugin); diff --git a/src/buttons/AbstractMoveButton.js b/src/buttons/AbstractMoveButton.js new file mode 100644 index 000000000..a889d304c --- /dev/null +++ b/src/buttons/AbstractMoveButton.js @@ -0,0 +1,117 @@ +import { SYSTEM } from '../data/system'; +import arrow from '../icons/arrow.svg'; +import { PressHandler } from '../utils/PressHandler'; +import { AbstractButton } from './AbstractButton'; + +export function getOrientedArrow(direction) { + let angle = 0; + switch (direction) { + // @formatter:off + case 'up': angle = 90; break; + case 'right': angle = 180; break; + case 'down': angle = -90; break; + default: angle = 0; break; + // @formatter:on + } + + return arrow.replace('rotate(0', `rotate(${angle}`); +} + +/** + * @summary Navigation bar move button class + * @extends PSV.buttons.AbstractButton + * @memberof PSV.buttons + */ +export class AbstractMoveButton extends AbstractButton { + + /** + * @param {PSV.components.Navbar} navbar + * @param {number} value + */ + constructor(navbar, value) { + super(navbar, 'psv-button--hover-scale psv-move-button'); + + /** + * @override + * @property {{longitude?: boolean, latitude?: boolean}} value + * @property {PressHandler} handler + */ + this.prop = { + ...this.prop, + value : value, + handler: new PressHandler(), + }; + + this.container.addEventListener('mousedown', this); + this.psv.container.addEventListener('mouseup', this); + this.psv.container.addEventListener('touchend', this); + } + + /** + * @override + */ + destroy() { + this.__onMouseUp(); + + this.psv.container.removeEventListener('mouseup', this); + this.psv.container.removeEventListener('touchend', this); + + super.destroy(); + } + + /** + * @summary Handles events + * @param {Event} e + * @private + */ + handleEvent(e) { + /* eslint-disable */ + switch (e.type) { + // @formatter:off + case 'mousedown': this.__onMouseDown(); break; + case 'mouseup': this.__onMouseUp(); break; + case 'touchend': this.__onMouseUp(); break; + // @formatter:on + } + /* eslint-enable */ + } + + /** + * @override + */ + isSupported() { + return { initial: true, promise: SYSTEM.isTouchEnabled.then(enabled => !enabled) }; + } + + /** + * @override + */ + onClick() { + // nothing + } + + /** + * @private + */ + __onMouseDown() { + if (!this.prop.enabled) { + return; + } + + this.psv.__stopAll(); + this.psv.dynamics.position.roll(this.prop.value); + this.prop.handler.down(); + } + + /** + * @private + */ + __onMouseUp() { + if (!this.prop.enabled) { + return; + } + + this.prop.handler.up(() => this.psv.dynamics.position.stop()); + } + +} diff --git a/src/buttons/AbstractZoomButton.js b/src/buttons/AbstractZoomButton.js index 45c18c8d0..2625ef89e 100644 --- a/src/buttons/AbstractZoomButton.js +++ b/src/buttons/AbstractZoomButton.js @@ -1,5 +1,5 @@ -import { Animation } from '../Animation'; import { SYSTEM } from '../data/system'; +import { PressHandler } from '../utils/PressHandler'; import { AbstractButton } from './AbstractButton'; /** @@ -18,17 +18,13 @@ export class AbstractZoomButton extends AbstractButton { /** * @override - * @property {number} value - * @property {boolean} buttondown - * @property {*} longPressTimeout - * @property {PSV.Animation} longPressAnimation + * @property {boolean} value + * @property {PressHandler} handler */ this.prop = { ...this.prop, - value : value, - buttondown : false, - longPressTimeout : null, - longPressAnimation: null, + value : value, + handler: new PressHandler(), }; this.container.addEventListener('mousedown', this); @@ -80,8 +76,6 @@ export class AbstractZoomButton extends AbstractButton { } /** - * @summary Handles click events - * @description Zooms in and register long press timer * @private */ __onMouseDown() { @@ -89,56 +83,19 @@ export class AbstractZoomButton extends AbstractButton { return; } - this.prop.buttondown = true; - this.prop.longPressTimeout = setTimeout(() => this.__startLongPressInterval(), 100); + this.psv.dynamics.zoom.roll(this.prop.value); + this.prop.handler.down(); } /** - * @summary Continues zooming as long as the user presses the button - * @private - */ - __startLongPressInterval() { - if (!this.prop.buttondown) { - return; - } - - const end = this.prop.value < 0 ? 0 : 100; - - this.prop.longPressAnimation = new Animation({ - properties: { - zoom: { start: this.psv.prop.zoomLvl, end: end }, - }, - duration : 1500 * Math.abs(this.psv.prop.zoomLvl - end) / 100, - easing : 'linear', - onTick : (properties) => { - this.psv.zoom(properties.zoom); - }, - }) - .catch(() => {}); // ignore cancellation - } - - /** - * @summary Handles mouse up events * @private */ __onMouseUp() { - if (!this.prop.enabled || !this.prop.buttondown) { + if (!this.prop.enabled) { return; } - if (this.prop.longPressAnimation) { - this.prop.longPressAnimation.cancel(); - this.prop.longPressAnimation = null; - } - else { - this.psv.zoom(this.psv.prop.zoomLvl + this.prop.value * this.psv.config.zoomButtonIncrement); - } - - if (this.prop.longPressTimeout) { - clearTimeout(this.prop.longPressTimeout); - } - - this.prop.buttondown = false; + this.prop.handler.up(() => this.psv.dynamics.zoom.stop()); } } diff --git a/src/buttons/MoveDownButton.js b/src/buttons/MoveDownButton.js new file mode 100644 index 000000000..ac79b6fd2 --- /dev/null +++ b/src/buttons/MoveDownButton.js @@ -0,0 +1,20 @@ +import { AbstractMoveButton, getOrientedArrow } from './AbstractMoveButton'; + +/** + * @summary Navigation bar move down button class + * @extends PSV.buttons.AbstractMoveButton + * @memberof PSV.buttons + */ +export class MoveDownButton extends AbstractMoveButton { + + static id = 'moveDown'; + static icon = getOrientedArrow('down'); + + /** + * @param {PSV.components.Navbar} navbar + */ + constructor(navbar) { + super(navbar, { latitude: true }); + } + +} diff --git a/src/buttons/MoveLeftButton.js b/src/buttons/MoveLeftButton.js new file mode 100644 index 000000000..e8352bef2 --- /dev/null +++ b/src/buttons/MoveLeftButton.js @@ -0,0 +1,20 @@ +import { AbstractMoveButton, getOrientedArrow } from './AbstractMoveButton'; + +/** + * @summary Navigation bar move left button class + * @extends PSV.buttons.AbstractMoveButton + * @memberof PSV.buttons + */ +export class MoveLeftButton extends AbstractMoveButton { + + static id = 'moveLeft'; + static icon = getOrientedArrow('left'); + + /** + * @param {PSV.components.Navbar} navbar + */ + constructor(navbar) { + super(navbar, { longitude: true }); + } + +} diff --git a/src/buttons/MoveRightButton.js b/src/buttons/MoveRightButton.js new file mode 100644 index 000000000..34743d461 --- /dev/null +++ b/src/buttons/MoveRightButton.js @@ -0,0 +1,20 @@ +import { AbstractMoveButton, getOrientedArrow } from './AbstractMoveButton'; + +/** + * @summary Navigation bar move right button class + * @extends PSV.buttons.AbstractMoveButton + * @memberof PSV.buttons + */ +export class MoveRightButton extends AbstractMoveButton { + + static id = 'moveRight'; + static icon = getOrientedArrow('right'); + + /** + * @param {PSV.components.Navbar} navbar + */ + constructor(navbar) { + super(navbar, { longitude: false }); + } + +} diff --git a/src/buttons/MoveUpButton.js b/src/buttons/MoveUpButton.js new file mode 100644 index 000000000..bdbf5c347 --- /dev/null +++ b/src/buttons/MoveUpButton.js @@ -0,0 +1,20 @@ +import { AbstractMoveButton, getOrientedArrow } from './AbstractMoveButton'; + +/** + * @summary Navigation bar move up button class + * @extends PSV.buttons.AbstractMoveButton + * @memberof PSV.buttons + */ +export class MoveUpButton extends AbstractMoveButton { + + static id = 'moveUp'; + static icon = getOrientedArrow('up'); + + /** + * @param {PSV.components.Navbar} navbar + */ + constructor(navbar) { + super(navbar, { latitude: false }); + } + +} diff --git a/src/buttons/ZoomInButton.js b/src/buttons/ZoomInButton.js index 53c9adc42..ca5a1bda9 100644 --- a/src/buttons/ZoomInButton.js +++ b/src/buttons/ZoomInButton.js @@ -15,7 +15,7 @@ export class ZoomInButton extends AbstractZoomButton { * @param {PSV.components.Navbar} navbar */ constructor(navbar) { - super(navbar, 1); + super(navbar, false); } } diff --git a/src/buttons/ZoomOutButton.js b/src/buttons/ZoomOutButton.js index 4942caac1..3d4393dfd 100644 --- a/src/buttons/ZoomOutButton.js +++ b/src/buttons/ZoomOutButton.js @@ -15,7 +15,7 @@ export class ZoomOutButton extends AbstractZoomButton { * @param {PSV.components.Navbar} navbar */ constructor(navbar) { - super(navbar, -1); + super(navbar, true); } } diff --git a/src/buttons/ZoomRangeButton.js b/src/buttons/ZoomRangeButton.js index a9fc36cd0..f193ae125 100644 --- a/src/buttons/ZoomRangeButton.js +++ b/src/buttons/ZoomRangeButton.js @@ -59,7 +59,7 @@ export class ZoomRangeButton extends AbstractButton { this.psv.on(EVENTS.ZOOM_UPDATED, this); if (this.psv.prop.ready) { - this.__moveZoomValue(this.psv.prop.zoomLvl); + this.__moveZoomValue(this.psv.getZoomLevel()); } else { this.psv.once(EVENTS.READY, this); @@ -103,7 +103,7 @@ export class ZoomRangeButton extends AbstractButton { case 'mouseup': this.__stopZoomChange(e); break; case 'touchend': this.__stopZoomChange(e); break; case EVENTS.ZOOM_UPDATED: this.__moveZoomValue(e.args[0]); break; - case EVENTS.READY: this.__moveZoomValue(this.psv.prop.zoomLvl); break; + case EVENTS.READY: this.__moveZoomValue(this.psv.getZoomLevel()); break; // @formatter:on } /* eslint-enable */ diff --git a/src/components/Navbar.js b/src/components/Navbar.js index 2fcd89b27..0ccc5441d 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -3,6 +3,10 @@ import { CustomButton } from '../buttons/CustomButton'; import { DownloadButton } from '../buttons/DownloadButton'; import { FullscreenButton } from '../buttons/FullscreenButton'; import { MenuButton } from '../buttons/MenuButton'; +import { MoveDownButton } from '../buttons/MoveDownButton'; +import { MoveLeftButton } from '../buttons/MoveLeftButton'; +import { MoveRightButton } from '../buttons/MoveRightButton'; +import { MoveUpButton } from '../buttons/MoveUpButton'; import { ZoomInButton } from '../buttons/ZoomInButton'; import { ZoomOutButton } from '../buttons/ZoomOutButton'; import { ZoomRangeButton } from '../buttons/ZoomRangeButton'; @@ -39,6 +43,10 @@ export function registerButton(button) { ZoomOutButton, DownloadButton, FullscreenButton, + MoveRightButton, + MoveLeftButton, + MoveUpButton, + MoveDownButton, ].forEach(registerButton); /** @@ -93,6 +101,12 @@ export class Navbar extends AbstractComponent { new ZoomRangeButton(this); new ZoomInButton(this); } + else if (button === 'move') { + new MoveLeftButton(this); + new MoveRightButton(this); + new MoveUpButton(this); + new MoveDownButton(this); + } else { throw new PSVError('Unknown button ' + button); } diff --git a/src/components/Notification.js b/src/components/Notification.js index fea669a13..a2fae1b20 100644 --- a/src/components/Notification.js +++ b/src/components/Notification.js @@ -65,7 +65,7 @@ export class Notification extends AbstractComponent { } if (typeof config === 'string') { - config = { content: config }; // eslint-disable-line no-param-reassign + config = { content: config }; } this.content.innerHTML = config.content; diff --git a/src/components/Overlay.js b/src/components/Overlay.js index 65c1f87ce..cb62ff0ae 100644 --- a/src/components/Overlay.js +++ b/src/components/Overlay.js @@ -104,7 +104,7 @@ export class Overlay extends AbstractComponent { */ show(config) { if (typeof config === 'string') { - config = { text: config }; // eslint-disable-line no-param-reassign + config = { text: config }; } this.prop.contentId = config.id; diff --git a/src/components/Panel.js b/src/components/Panel.js index b56ff847b..365ab1d2e 100644 --- a/src/components/Panel.js +++ b/src/components/Panel.js @@ -138,7 +138,7 @@ export class Panel extends AbstractComponent { */ show(config) { if (typeof config === 'string') { - config = { content: config }; // eslint-disable-line no-param-reassign + config = { content: config }; } this.prop.contentId = config.id; diff --git a/src/data/config.js b/src/data/config.js index 9468ea86e..252594f22 100644 --- a/src/data/config.js +++ b/src/data/config.js @@ -24,13 +24,12 @@ export const DEFAULTS = { sphereCorrection : null, sphereCorrectionReorder: false, moveSpeed : 1, - zoomButtonIncrement: 2, + zoomSpeed : 1, autorotateDelay : null, autorotateSpeed : '2rpm', autorotateLat : null, moveInertia : true, mousewheel : true, - mousewheelSpeed : 1, mousemove : true, captureCursor : false, mousewheelCtrlKey : false, @@ -41,9 +40,8 @@ export const DEFAULTS = { withCredentials : false, navbar : [ 'autorotate', - 'zoomOut', - 'zoomRange', - 'zoomIn', + 'zoom', + 'move', 'download', 'caption', 'fullscreen', @@ -85,6 +83,15 @@ export const READONLY_OPTIONS = { plugins : 'Cannot change plugins', }; +/** + * @summary List of deprecated options and their warning messages + * @private + */ +export const DEPRECATED_OPTIONS = { + zoomButtonIncrement: 'zoomButtonIncrement is deprecated, use zoomSpeed', + mousewheelSpeed : 'mousewheelSpeed is deprecated, use zoomSpeed', +}; + /** * @summary Parsers/validators for each option * @private @@ -108,7 +115,6 @@ export const CONFIG_PARSERS = { // minFov and maxFov must be ordered if (config.maxFov < minFov) { logWarn('maxFov cannot be lower than minFov'); - // eslint-disable-next-line no-param-reassign minFov = config.maxFov; } // minFov between 1 and 179 @@ -117,7 +123,6 @@ export const CONFIG_PARSERS = { maxFov : (maxFov, config) => { // minFov and maxFov must be ordered if (maxFov < config.minFov) { - // eslint-disable-next-line no-param-reassign maxFov = config.minFov; } // maxFov between 1 and 179 @@ -191,6 +196,11 @@ export function getConfig(options) { const config = {}; each(tempConfig, (value, key) => { + if (DEPRECATED_OPTIONS[key]) { + logWarn(DEPRECATED_OPTIONS[key]); + return; + } + if (!Object.prototype.hasOwnProperty.call(DEFAULTS, key)) { throw new PSVError(`Unknown option ${key}`); } diff --git a/src/data/constants.js b/src/data/constants.js index 7392a5258..2fb5660c0 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -133,6 +133,7 @@ export const EVENTS = { * @memberof PSV * @summary Triggered before a render, used to modify the view * @param {number} timestamp - time provided by requestAnimationFrame + * @param {number} elapsed - time elapsed from the previous frame */ BEFORE_RENDER : 'before-render', /** diff --git a/src/icons/arrow.svg b/src/icons/arrow.svg new file mode 100644 index 000000000..8476dc67e --- /dev/null +++ b/src/icons/arrow.svg @@ -0,0 +1 @@ + diff --git a/src/plugins/autorotate-keypoints/index.js b/src/plugins/autorotate-keypoints/index.js index 89a16b222..91edc4619 100644 --- a/src/plugins/autorotate-keypoints/index.js +++ b/src/plugins/autorotate-keypoints/index.js @@ -78,10 +78,12 @@ export default class AutorotateKeypointsPlugin extends AbstractPlugin { } this.psv.on(CONSTANTS.EVENTS.AUTOROTATE, this); + this.psv.on(CONSTANTS.EVENTS.BEFORE_RENDER, this); } destroy() { this.psv.off(CONSTANTS.EVENTS.AUTOROTATE, this); + this.psv.off(CONSTANTS.EVENTS.BEFORE_RENDER, this); delete this.keypoints; delete this.state; @@ -93,6 +95,9 @@ export default class AutorotateKeypointsPlugin extends AbstractPlugin { if (e.type === CONSTANTS.EVENTS.AUTOROTATE) { this.__configure(); } + else if (e.type === CONSTANTS.EVENTS.BEFORE_RENDER) { + this.__beforeRender(e.args[0]); + } } /** @@ -109,11 +114,9 @@ export default class AutorotateKeypointsPlugin extends AbstractPlugin { if (this.keypoints) { this.keypoints.forEach((pt, i) => { if (typeof pt === 'string') { - // eslint-disable-next-line no-param-reassign pt = { markerId: pt }; } else if (utils.isExtendedPosition(pt)) { - // eslint-disable-next-line no-param-reassign pt = { position: pt }; } if (pt.markerId) { @@ -151,6 +154,9 @@ export default class AutorotateKeypointsPlugin extends AbstractPlugin { return; } + // cancel core rotation + this.psv.dynamics.position.stop(); + this.state = { idx : -1, curve : [], @@ -164,17 +170,23 @@ export default class AutorotateKeypointsPlugin extends AbstractPlugin { }; if (this.config.startFromClosest) { + const currentPosition = serializePt(this.psv.getPosition()); const index = this.__findMinIndex(this.keypoints, (keypoint) => { - return utils.greatArcDistance(keypoint.position, serializePt(this.psv.prop.position)); + return utils.greatArcDistance(keypoint.position, currentPosition); }); this.keypoints.push(...this.keypoints.splice(0, index)); } + } - const autorotateCb = (e, timestamp) => { + /** + * @private + */ + __beforeRender(timestamp) { + if (this.psv.isAutorotateEnabled()) { // initialisation if (!this.state.startTime) { - this.state.endPt = serializePt(this.psv.prop.position); + this.state.endPt = serializePt(this.psv.getPosition()); this.__nextStep(); this.state.startTime = timestamp; @@ -182,11 +194,7 @@ export default class AutorotateKeypointsPlugin extends AbstractPlugin { } this.__nextFrame(timestamp); - }; - - this.psv.off(CONSTANTS.EVENTS.BEFORE_RENDER, this.psv.prop.autorotateCb); - this.psv.prop.autorotateCb = autorotateCb; - this.psv.on(CONSTANTS.EVENTS.BEFORE_RENDER, this.psv.prop.autorotateCb); + } } /** @@ -249,9 +257,10 @@ export default class AutorotateKeypointsPlugin extends AbstractPlugin { // one point before and two points before current const workPoints = []; if (this.state.idx === -1) { + const currentPosition = serializePt(this.psv.getPosition()); workPoints.push( - serializePt(this.psv.prop.position), - serializePt(this.psv.prop.position), + currentPosition, + currentPosition, this.keypoints[0].position, this.keypoints[1].position ); diff --git a/src/plugins/gyroscope/compass.svg b/src/plugins/gyroscope/compass.svg index 2a86ba894..a2587a963 100644 --- a/src/plugins/gyroscope/compass.svg +++ b/src/plugins/gyroscope/compass.svg @@ -1 +1 @@ - + diff --git a/src/plugins/gyroscope/index.js b/src/plugins/gyroscope/index.js index 9bc01ebc7..fdcb2f668 100644 --- a/src/plugins/gyroscope/index.js +++ b/src/plugins/gyroscope/index.js @@ -20,6 +20,7 @@ DEFAULTS.navbar.splice(-1, 0, GyroscopeButton.id); DEFAULTS.lang[GyroscopeButton.id] = 'Gyroscope'; registerButton(GyroscopeButton); +const direction = new THREE.Vector3(); /** * @summary Adds gyroscope controls on mobile devices @@ -52,13 +53,13 @@ export default class GyroscopePlugin extends AbstractPlugin { * @private * @property {Promise} isSupported - indicates of the gyroscope API is available * @property {number} alphaOffset - current alpha offset for gyroscope controls - * @property {Function} orientationCb - update callback of the device orientation + * @property {boolean} enabled * @property {boolean} config_moveInertia - original config "moveInertia" */ this.prop = { isSupported : this.__checkSupport(), alphaOffset : 0, - orientationCb : null, + enabled : false, config_moveInertia: true, }; @@ -80,6 +81,7 @@ export default class GyroscopePlugin extends AbstractPlugin { this.psv.on(CONSTANTS.EVENTS.STOP_ALL, this); this.psv.on(CONSTANTS.EVENTS.BEFORE_ROTATE, this); + this.psv.on(CONSTANTS.EVENTS.BEFORE_RENDER, this); } /** @@ -88,6 +90,7 @@ export default class GyroscopePlugin extends AbstractPlugin { 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(); @@ -105,8 +108,11 @@ export default class GyroscopePlugin extends AbstractPlugin { case CONSTANTS.EVENTS.STOP_ALL: this.stop(); break; + case CONSTANTS.EVENTS.BEFORE_RENDER: + this.__onBeforeRender(); + break; case CONSTANTS.EVENTS.BEFORE_ROTATE: - this.__onRotate(e); + this.__onBeforeRotate(e); break; default: break; @@ -118,7 +124,7 @@ export default class GyroscopePlugin extends AbstractPlugin { * @returns {boolean} */ isEnabled() { - return !!this.prop.orientationCb; + return this.prop.enabled; } /** @@ -154,7 +160,21 @@ export default class GyroscopePlugin extends AbstractPlugin { this.prop.config_moveInertia = this.psv.config.moveInertia; this.psv.config.moveInertia = false; - this.__configure(); + // enable gyro controls + if (!this.controls) { + this.controls = new DeviceOrientationControls(new THREE.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; /** * @event gyroscope-updated @@ -174,9 +194,7 @@ export default class GyroscopePlugin extends AbstractPlugin { if (this.isEnabled()) { this.controls.disconnect(); - this.psv.off(CONSTANTS.EVENTS.BEFORE_RENDER, this.prop.orientationCb); - this.prop.orientationCb = null; - + this.prop.enabled = false; this.psv.config.moveInertia = this.prop.config_moveInertia; this.trigger(GyroscopePlugin.EVENTS.GYROSCOPE_UPDATED, false); @@ -196,54 +214,38 @@ export default class GyroscopePlugin extends AbstractPlugin { } /** - * @summary Attaches the {@link external:THREE.DeviceOrientationControls} to the camera + * @summary Handles gyro movements * @private */ - __configure() { - if (!this.controls) { - this.controls = new DeviceOrientationControls(this.psv.renderer.camera); - } - else { - this.controls.connect(); + __onBeforeRender() { + if (!this.isEnabled()) { + return; } - // force reset - this.controls.deviceOrientation = null; - this.controls.screenOrientation = 0; - this.controls.alphaOffset = 0; - this.prop.alphaOffset = this.config.absolutePosition ? 0 : null; - - this.prop.orientationCb = () => { - if (!this.controls.deviceOrientation) { - return; - } - - // on first run compute the offset depending on the current viewer position and device orientation - if (this.prop.alphaOffset === null) { - this.controls.update(); - - const direction = new THREE.Vector3(); - this.psv.renderer.camera.getWorldDirection(direction); - - const sphericalCoords = this.psv.dataHelper.vector3ToSphericalCoords(direction); - this.prop.alphaOffset = sphericalCoords.longitude - this.psv.prop.position.longitude; - } - else { - this.controls.alphaOffset = this.prop.alphaOffset; - this.controls.update(); - - this.psv.renderer.camera.getWorldDirection(this.psv.prop.direction); - this.psv.prop.direction.multiplyScalar(CONSTANTS.SPHERE_RADIUS); - - const sphericalCoords = this.psv.dataHelper.vector3ToSphericalCoords(this.psv.prop.direction); - this.psv.prop.position.longitude = sphericalCoords.longitude; - this.psv.prop.position.latitude = sphericalCoords.latitude; + if (!this.controls.deviceOrientation) { + return; + } - this.psv.needsUpdate(); - } - }; + // 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); - this.psv.on(CONSTANTS.EVENTS.BEFORE_RENDER, this.prop.orientationCb); + const sphericalCoords = this.psv.dataHelper.vector3ToSphericalCoords(direction); + this.prop.alphaOffset = sphericalCoords.longitude - this.psv.getPosition().longitude; + } + else { + this.controls.alphaOffset = this.prop.alphaOffset; + this.controls.update(); + this.controls.object.getWorldDirection(direction); + + const sphericalCoords = this.psv.dataHelper.vector3ToSphericalCoords(direction); + // TODO use dynamic goto for smooth movement + this.psv.dynamics.position.setValue({ + longitude: sphericalCoords.longitude, + latitude : -sphericalCoords.latitude, + }); + } } /** @@ -251,12 +253,12 @@ export default class GyroscopePlugin extends AbstractPlugin { * @param {external:uEvent.Event} e * @private */ - __onRotate(e) { + __onBeforeRotate(e) { if (this.isEnabled()) { e.preventDefault(); if (this.config.touchmove) { - this.prop.alphaOffset -= e.args[0].longitude - this.psv.prop.position.longitude; + this.prop.alphaOffset -= e.args[0].longitude - this.psv.getPosition().longitude; } } } diff --git a/src/plugins/markers/index.js b/src/plugins/markers/index.js index 1930d69cb..07732a086 100644 --- a/src/plugins/markers/index.js +++ b/src/plugins/markers/index.js @@ -694,7 +694,7 @@ export default class MarkersPlugin extends AbstractPlugin { */ __getMarkerPosition(marker, scale = 1) { if (marker.isPoly()) { - return this.psv.dataHelper.vector3ToViewerCoords(this.psv.dataHelper.sphericalCoordsToVector3(marker.props.position)); + return this.psv.dataHelper.sphericalCoordsToViewerCoords(marker.props.position); } else { const position = this.psv.dataHelper.vector3ToViewerCoords(marker.props.positions3D[0]); diff --git a/src/plugins/visible-range/index.js b/src/plugins/visible-range/index.js index 1926082f2..51a12e6e8 100644 --- a/src/plugins/visible-range/index.js +++ b/src/plugins/visible-range/index.js @@ -31,7 +31,7 @@ export default class VisibleRangePlugin extends AbstractPlugin { this.config = { latitudeRange : null, longitudeRange: null, - usePanoData: false, + usePanoData : false, }; if (options) { @@ -41,6 +41,7 @@ export default class VisibleRangePlugin extends AbstractPlugin { } this.psv.on(CONSTANTS.EVENTS.PANORAMA_LOADED, this); + this.psv.on(CONSTANTS.EVENTS.ZOOM_UPDATED, this); this.psv.on(CONSTANTS.CHANGE_EVENTS.GET_ANIMATE_POSITION, this); this.psv.on(CONSTANTS.CHANGE_EVENTS.GET_ROTATE_POSITION, this); } @@ -50,6 +51,7 @@ export default class VisibleRangePlugin extends AbstractPlugin { */ destroy() { this.psv.off(CONSTANTS.EVENTS.PANORAMA_LOADED, this); + this.psv.off(CONSTANTS.EVENTS.ZOOM_UPDATED, this); this.psv.off(CONSTANTS.CHANGE_EVENTS.GET_ANIMATE_POSITION, this); this.psv.off(CONSTANTS.CHANGE_EVENTS.GET_ROTATE_POSITION, this); @@ -80,6 +82,13 @@ export default class VisibleRangePlugin extends AbstractPlugin { this.setRangesFromPanoData(); } } + else if (e.type === CONSTANTS.EVENTS.ZOOM_UPDATED) { + const currentPosition = this.psv.getPosition(); + const { rangedPosition } = this.applyRanges(currentPosition); + if (currentPosition.longitude !== rangedPosition.longitude || currentPosition.latitude !== rangedPosition.latitude) { + this.psv.rotate(rangedPosition); + } + } } /** @@ -90,13 +99,11 @@ export default class VisibleRangePlugin extends AbstractPlugin { // latitude range must have two values if (range && range.length !== 2) { utils.logWarn('latitude range must have exactly two elements'); - // eslint-disable-next-line no-param-reassign range = null; } // latitude range must be ordered else if (range && range[0] > range[1]) { utils.logWarn('latitude range values must be ordered'); - // eslint-disable-next-line no-param-reassign range = [range[1], range[0]]; } // latitude range is between -PI/2 and PI/2 @@ -120,7 +127,6 @@ export default class VisibleRangePlugin extends AbstractPlugin { // longitude range must have two values if (range && range.length !== 2) { utils.logWarn('longitude range must have exactly two elements'); - // eslint-disable-next-line no-param-reassign range = null; } // longitude range is between 0 and 2*PI diff --git a/src/services/DataHelper.js b/src/services/DataHelper.js index 5c2fbad40..2750aa09d 100644 --- a/src/services/DataHelper.js +++ b/src/services/DataHelper.js @@ -4,6 +4,9 @@ import { PSVError } from '../PSVError'; import { parseAngle, parseSpeed } from '../utils'; import { AbstractService } from './AbstractService'; +const vector2 = new THREE.Vector2(); +const vector3 = new THREE.Vector3(); + /** * @summary Collections of data converters for the current viewer * @extends PSV.services.AbstractService @@ -107,14 +110,17 @@ export class DataHelper extends AbstractService { /** * @summary Converts spherical radians coordinates to a THREE.Vector3 * @param {PSV.Position} position + * @param {external:THREE.Vector3} [vector] * @returns {external:THREE.Vector3} */ - sphericalCoordsToVector3(position) { - return new THREE.Vector3( - SPHERE_RADIUS * -Math.cos(position.latitude) * Math.sin(position.longitude), - SPHERE_RADIUS * Math.sin(position.latitude), - SPHERE_RADIUS * Math.cos(position.latitude) * Math.cos(position.longitude) - ); + sphericalCoordsToVector3(position, vector) { + if (!vector) { + vector = new THREE.Vector3(); + } + vector.x = SPHERE_RADIUS * -Math.cos(position.latitude) * Math.sin(position.longitude); + vector.y = SPHERE_RADIUS * Math.sin(position.latitude); + vector.z = SPHERE_RADIUS * Math.cos(position.latitude) * Math.cos(position.longitude); + return vector; } /** @@ -138,12 +144,10 @@ export class DataHelper extends AbstractService { * @returns {external:THREE.Vector3} */ viewerCoordsToVector3(viewerPoint) { - const screen = new THREE.Vector2( - 2 * viewerPoint.x / this.prop.size.width - 1, - -2 * viewerPoint.y / this.prop.size.height + 1 - ); + vector2.x = 2 * viewerPoint.x / this.prop.size.width - 1; + vector2.y = -2 * viewerPoint.y / this.prop.size.height + 1; - this.psv.renderer.raycaster.setFromCamera(screen, this.psv.renderer.camera); + this.psv.renderer.raycaster.setFromCamera(vector2, this.psv.renderer.camera); const intersects = this.psv.renderer.raycaster.intersectObjects(this.psv.renderer.scene.children, true); @@ -170,6 +174,15 @@ export class DataHelper extends AbstractService { }; } + /** + * @summary Converts spherical radians coordinates to position on the viewer + * @param {PSV.Position} position + * @returns {PSV.Point} + */ + sphericalCoordsToViewerCoords(position) { + return this.vector3ToViewerCoords(this.sphericalCoordsToVector3(position, vector3)); + } + /** * @summary Converts x/y to latitude/longitude if present and ensure boundaries * @param {PSV.ExtendedPosition} position diff --git a/src/services/EventsHandler.js b/src/services/EventsHandler.js index d0c51d201..9fa22c0f6 100644 --- a/src/services/EventsHandler.js +++ b/src/services/EventsHandler.js @@ -1,4 +1,4 @@ -import { Animation } from '../Animation'; +import * as THREE from 'three'; import { ACTIONS, CTRLZOOM_TIMEOUT, @@ -7,14 +7,16 @@ import { IDS, INERTIA_WINDOW, LONGTOUCH_DELAY, - TWOFINGERSOVERLAY_DELAY, - MOVE_THRESHOLD + MOVE_THRESHOLD, + TWOFINGERSOVERLAY_DELAY } from '../data/constants'; import { SYSTEM } from '../data/system'; import gestureIcon from '../icons/gesture.svg'; import mousewheelIcon from '../icons/mousewheel.svg'; import { clone, distance, getClosest, getEventKey, isFullscreenEnabled, normalizeWheel, throttle } from '../utils'; +import { PressHandler } from '../utils/PressHandler'; import { AbstractService } from './AbstractService'; +import { Animation } from '../Animation'; /** * @summary Events handler @@ -40,6 +42,7 @@ export class EventsHandler extends AbstractService { * @property {number} mouseY - current y position of the cursor * @property {number[][]} mouseHistory - list of latest positions of the cursor, [time, x, y] * @property {number} pinchDist - distance between fingers when zooming + * @property {PressHandler} keyHandler * @property {boolean} ctrlKeyDown - when the Ctrl key is pressed * @property {PSV.ClickData} dblclickData - temporary storage of click data between two clicks * @property {number} dblclickTimeout - timeout id for double click @@ -57,6 +60,7 @@ export class EventsHandler extends AbstractService { mouseY : 0, mouseHistory : [], pinchDist : 0, + keyHandler : new PressHandler(), ctrlKeyDown : false, dblclickData : null, dblclickTimeout : null, @@ -200,32 +204,24 @@ export class EventsHandler extends AbstractService { return; } - let dLong = 0; - let dLat = 0; - let dZoom = 0; - - /* eslint-disable */ - switch (this.config.keyboard[key]) { - // @formatter:off - case ACTIONS.ROTATE_LAT_UP : dLat = 0.01; break; - case ACTIONS.ROTATE_LAT_DOWN : dLat = -0.01; break; - case ACTIONS.ROTATE_LONG_RIGHT: dLong = 0.01; break; - case ACTIONS.ROTATE_LONG_LEFT : dLong = -0.01; break; - case ACTIONS.ZOOM_IN : dZoom = 1; break; - case ACTIONS.ZOOM_OUT : dZoom = -1; break; - case ACTIONS.TOGGLE_AUTOROTATE: this.psv.toggleAutorotate(); break; - // @formatter:on + if (this.config.keyboard[key] === ACTIONS.TOGGLE_AUTOROTATE) { + this.psv.toggleAutorotate(); } - /* eslint-enable */ + else if (this.config.keyboard[key] && !this.state.keyHandler.time) { + /* eslint-disable */ + switch (this.config.keyboard[key]) { + // @formatter:off + case ACTIONS.ROTATE_LAT_UP: this.psv.dynamics.position.roll({latitude: false}); break; + case ACTIONS.ROTATE_LAT_DOWN: this.psv.dynamics.position.roll({latitude: true}); break; + case ACTIONS.ROTATE_LONG_RIGHT: this.psv.dynamics.position.roll({longitude: false}); break; + case ACTIONS.ROTATE_LONG_LEFT: this.psv.dynamics.position.roll({longitude: true}); break; + case ACTIONS.ZOOM_IN: this.psv.dynamics.zoom.roll(false); break; + case ACTIONS.ZOOM_OUT: this.psv.dynamics.zoom.roll(true); break; + // @formatter:on + } + /* eslint-enable */ - if (dZoom !== 0) { - this.psv.zoom(this.prop.zoomLvl + dZoom * this.config.zoomButtonIncrement); - } - else if (dLat !== 0 || dLong !== 0) { - this.psv.rotate({ - longitude: this.prop.position.longitude + dLong * this.prop.moveSpeed * this.prop.hFov, - latitude : this.prop.position.latitude + dLat * this.prop.moveSpeed * this.prop.vFov, - }); + this.state.keyHandler.down(); } } @@ -235,6 +231,15 @@ export class EventsHandler extends AbstractService { */ __onKeyUp() { this.state.ctrlKeyDown = false; + + if (!this.state.keyboardEnabled) { + return; + } + + this.state.keyHandler.up(() => { + this.psv.dynamics.position.stop(); + this.psv.dynamics.zoom.stop(); + }); } /** @@ -378,9 +383,9 @@ export class EventsHandler extends AbstractService { if (!this.prop.twofingersTimeout) { this.prop.twofingersTimeout = setTimeout(() => { this.psv.overlay.show({ - id: IDS.TWO_FINGERS, + id : IDS.TWO_FINGERS, image: gestureIcon, - text: this.config.lang.twoFingers, + text : this.config.lang.twoFingers, }); }, TWOFINGERSOVERLAY_DELAY); } @@ -433,9 +438,9 @@ export class EventsHandler extends AbstractService { if (this.config.mousewheelCtrlKey && !this.state.ctrlKeyDown) { this.psv.overlay.show({ - id: IDS.CTRL_ZOOM, + id : IDS.CTRL_ZOOM, image: mousewheelIcon, - text: this.config.lang.ctrlZoom, + text : this.config.lang.ctrlZoom, }); clearTimeout(this.state.ctrlZoomTimeout); @@ -447,10 +452,9 @@ export class EventsHandler extends AbstractService { evt.preventDefault(); evt.stopPropagation(); - const delta = normalizeWheel(evt).spinY * 5; - + const delta = normalizeWheel(evt).spinY * 5 * this.config.zoomSpeed; if (delta !== 0) { - this.psv.zoom(this.prop.zoomLvl - delta * this.config.mousewheelSpeed); + this.psv.dynamics.zoom.step(-delta, 5); } } @@ -659,13 +663,14 @@ export class EventsHandler extends AbstractService { const y = evt.clientY; const rotation = { - longitude: (x - this.state.mouseX) / this.prop.size.width * this.prop.moveSpeed * this.prop.hFov * SYSTEM.pixelRatio, - latitude : (y - this.state.mouseY) / this.prop.size.height * this.prop.moveSpeed * this.prop.vFov * SYSTEM.pixelRatio, + longitude: (x - this.state.mouseX) / this.prop.size.width * this.config.moveSpeed * THREE.Math.degToRad(this.prop.hFov), + latitude : (y - this.state.mouseY) / this.prop.size.height * this.config.moveSpeed * THREE.Math.degToRad(this.prop.vFov), }; + const currentPosition = this.psv.getPosition(); this.psv.rotate({ - longitude: this.prop.position.longitude - rotation.longitude, - latitude : this.prop.position.latitude + rotation.latitude, + longitude: currentPosition.longitude - rotation.longitude, + latitude : currentPosition.latitude + rotation.latitude, }); this.state.mouseX = x; @@ -685,10 +690,10 @@ export class EventsHandler extends AbstractService { __moveAbsolute(evt) { if (this.state.moving) { const containerRect = this.psv.container.getBoundingClientRect(); - this.psv.rotate({ + this.psv.dynamics.position.goto({ longitude: ((evt.clientX - containerRect.left) / containerRect.width - 0.5) * Math.PI * 2, latitude : -((evt.clientY - containerRect.top) / containerRect.height - 0.5) * Math.PI, - }); + }, 10); } } @@ -703,9 +708,9 @@ export class EventsHandler extends AbstractService { const p2 = { x: evt.touches[1].clientX, y: evt.touches[1].clientY }; const p = distance(p1, p2); - const delta = 80 * (p - this.state.pinchDist) / this.prop.size.width; + const delta = 80 * (p - this.state.pinchDist) / this.prop.size.width * this.config.zoomSpeed; - this.psv.zoom(this.prop.zoomLvl + delta); + this.psv.zoom(this.psv.getZoomLevel() + delta); this.__move({ clientX: (p1.x + p2.x) / 2, diff --git a/src/services/Renderer.js b/src/services/Renderer.js index 09972aa38..1b9dad290 100644 --- a/src/services/Renderer.js +++ b/src/services/Renderer.js @@ -2,7 +2,7 @@ import * as THREE from 'three'; import { Animation } from '../Animation'; import { CUBE_VERTICES, EVENTS, SPHERE_RADIUS, SPHERE_VERTICES } from '../data/constants'; import { SYSTEM } from '../data/system'; -import { isExtendedPosition, isNil, logWarn } from '../utils'; +import { each, isExtendedPosition, isNil, logWarn } from '../utils'; import { AbstractService } from './AbstractService'; /** @@ -66,6 +66,12 @@ export class Renderer extends AbstractService { */ this.raycaster = null; + /** + * @member {number} + * @private + */ + this.timestamp = null; + /** * @member {HTMLElement} * @readonly @@ -139,7 +145,11 @@ export class Renderer extends AbstractService { * @package */ __renderLoop(timestamp) { - this.psv.trigger(EVENTS.BEFORE_RENDER, 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.needsUpdate) { this.render(); @@ -156,7 +166,7 @@ export class Renderer extends AbstractService { * @fires PSV.render */ render() { - this.prop.direction = this.psv.dataHelper.sphericalCoordsToVector3(this.prop.position); + this.psv.dataHelper.sphericalCoordsToVector3(this.psv.getPosition(), this.prop.direction); this.camera.position.set(0, 0, 0); this.camera.lookAt(this.prop.direction); @@ -381,21 +391,15 @@ export class Renderer extends AbstractService { // 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 THREE.Vector3(0, 1, 0); - group.rotateOnWorldAxis(verticalAxis, cleanPosition.longitude - this.prop.position.longitude); + group.rotateOnWorldAxis(verticalAxis, cleanPosition.longitude - currentPosition.longitude); // Latitude rotation along the camera horizontal axis const horizontalAxis = new THREE.Vector3(0, 1, 0).cross(this.camera.getWorldDirection(new THREE.Vector3())).normalize(); - group.rotateOnWorldAxis(horizontalAxis, cleanPosition.latitude - this.prop.position.latitude); - - // TODO: find a better way to handle ranges - if (this.config.latitudeRange || this.config.longitudeRange) { - this.config.longitudeRange = null; - this.config.latitudeRange = null; - logWarn('trying to perform transition with longitudeRange and/or latitudeRange, ranges cleared'); - } + group.rotateOnWorldAxis(horizontalAxis, cleanPosition.latitude - currentPosition.latitude); } group.add(mesh); @@ -405,7 +409,7 @@ export class Renderer extends AbstractService { return new Animation({ properties: { opacity: { start: 0.0, end: 1.0 }, - zoom : zoomProvided ? { start: this.prop.zoomLvl, end: options.zoom } : undefined, + zoom : zoomProvided ? { start: this.psv.getZoomLevel(), end: options.zoom } : undefined, }, duration : options.transition, easing : 'outCubic', diff --git a/src/services/TextureLoader.js b/src/services/TextureLoader.js index b790b4d10..6316c76c7 100644 --- a/src/services/TextureLoader.js +++ b/src/services/TextureLoader.js @@ -195,7 +195,6 @@ export class TextureLoader extends AbstractService { ) .then(({ img, xmpPanoData }) => { if (typeof newPanoData === 'function') { - // eslint-disable-next-line no-param-reassign newPanoData = newPanoData(img); } diff --git a/src/styles/navbar.scss b/src/styles/navbar.scss index 58b8a16e8..c39f38d96 100644 --- a/src/styles/navbar.scss +++ b/src/styles/navbar.scss @@ -74,3 +74,7 @@ .psv-container:not(.psv--is-touch) .psv-button--hover-scale:not(.psv-button--disabled):hover .psv-button-svg { transform: scale($psv-buttons-hover-scale); } + +.psv-move-button + .psv-move-button { + padding-left: 0; +} diff --git a/src/utils/Dynamic.js b/src/utils/Dynamic.js new file mode 100644 index 000000000..42198154f --- /dev/null +++ b/src/utils/Dynamic.js @@ -0,0 +1,191 @@ +import { bound } from './index'; + +/** + * @summary Represents a variable that can dynamically change with time (using requestAnimationFrame) + * @memberOf PSV + * @package + */ +export class Dynamic { + + static STOP = 0; + static INFINITE = 1; + static POSITION = 2; + + /** + * @param {Function} fn Callback function + * @param {number} [min] Minimum position + * @param {number} [max] Maximum position + */ + constructor(fn, min = -Infinity, max = Infinity) { + /** + * @type {Function} + * @private + * @readonly + */ + this.fn = fn; + + /** + * @type {number} + * @private + */ + this.mode = Dynamic.STOP; + + /** + * @type {number} + * @private + */ + this.speed = 0; + + /** + * @type {number} + * @private + */ + this.speedMult = 1; + + /** + * @type {number} + * @private + */ + this.currentSpeed = 0; + + /** + * @type {number} + * @private + */ + this.target = 0; + + /** + * @type {number} + * @readonly + */ + this.current = 0; + + /** + * @type {number} + * @private + */ + this.min = min; + + /** + * @type {number} + * @private + */ + this.max = max; + } + + /** + * Changes base speed + * @param {number} speed + */ + setSpeed(speed) { + this.speed = speed; + } + + /** + * Defines the target position + * @param {number} position + * @param {number} [speedMult=1] + */ + goto(position, speedMult = 1) { + this.mode = Dynamic.POSITION; + this.target = bound(position, this.min, this.max); + this.speedMult = speedMult; + } + + /** + * Increase/decrease the target position + * @param {number} step + * @param {number} [speedMult=1] + */ + step(step, speedMult = 1) { + if (this.mode !== Dynamic.POSITION) { + this.target = this.current; + } + this.goto(this.target + step, speedMult); + } + + /** + * Starts infinite movement + * @param {boolean} [invert=false] + * @param {number} [speedMult=1] + */ + roll(invert = false, speedMult = 1) { + this.mode = Dynamic.INFINITE; + this.target = invert ? -Infinity : Infinity; + this.speedMult = speedMult; + } + + /** + * Stops movement + */ + stop() { + this.mode = Dynamic.STOP; + } + + /** + * Defines the current position and immediately stops movement + * @param {number} values + */ + setValue(values) { + const next = bound(values, this.min, this.max); + this.target = next; + this.mode = Dynamic.STOP; + if (next !== this.current) { + this.current = next; + if (this.fn) { + this.fn(this.current); + } + return true; + } + return false; + } + + /** + * @package + */ + update(elapsed) { + // in position mode switch to stop mode when in the decceleration window + if (this.mode === Dynamic.POSITION) { + const dstStop = this.currentSpeed * this.currentSpeed / (this.speed * this.speedMult * 4); + if (Math.abs(this.target - this.current) <= dstStop) { + this.mode = Dynamic.STOP; + } + } + + // compute speed + let targetSpeed = this.mode === Dynamic.STOP ? 0 : this.speed * this.speedMult; + if (this.target < this.current) { + targetSpeed = -targetSpeed; + } + if (this.currentSpeed < targetSpeed) { + this.currentSpeed = Math.min(targetSpeed, this.currentSpeed + elapsed / 1000 * this.speed * this.speedMult * 2); + } + else if (this.currentSpeed > targetSpeed) { + this.currentSpeed = Math.max(targetSpeed, this.currentSpeed - elapsed / 1000 * this.speed * this.speedMult * 2); + } + + // compute new position + let next = null; + if (this.current > this.target && this.currentSpeed) { + next = Math.max(this.target, this.current + this.currentSpeed * elapsed / 1000); + } + else if (this.current < this.target && this.currentSpeed) { + next = Math.min(this.target, this.current + this.currentSpeed * elapsed / 1000); + } + + // apply value + if (next !== null) { + next = bound(next, this.min, this.max); + if (next !== this.current) { + this.current = next; + if (this.fn) { + this.fn(this.current); + } + return true; + } + } + + return false; + } + +} diff --git a/src/utils/DynamicXD.js b/src/utils/DynamicXD.js new file mode 100644 index 000000000..ae379af00 --- /dev/null +++ b/src/utils/DynamicXD.js @@ -0,0 +1,191 @@ +import { bound } from './math'; +import { each } from './misc'; + +/** + * @summary Implementation of {@link PSV.Dynamic} for any number of variables, unused + * @memberOf PSV + * @package + */ +export class DynamicXD { + + static STOP = 0; + static INFINITE = 1; + static POSITION = 2; + + get current() { + return this.reduce((values, _) => { + values[_.name] = _.current; + return values; + }, {}); + } + + constructor(fn, _) { + this.fn = fn; + this.mode = DynamicXD.STOP; + this.speed = 0; + this.speedMult = 1; + this.currentSpeed = 0; + this._ = {}; + each(_, (dim, name) => { + this._[name] = { + min : -Infinity, + max : Infinity, + ...dim, + name : name, + target : 0, + current: 0, + }; + }); + } + + forEach(fn) { + each(this._, fn); + } + + reduce(fn, init) { + return Object.keys(this._).reduce((acc, name) => fn(acc, this._[name]), init); + } + + /** + * Defines the target position + */ + goto(positions, speedMult = 1) { + this.mode = DynamicXD.POSITION; + this.speedMult = speedMult; + this.forEach((_) => { + if (_.name in positions) { + _.target = bound(positions[_.name], _.min, _.max); + } + }); + } + + /** + * Increase/decrease the target position + */ + step(steps, speedMult = 1) { + if (this.mode !== DynamicXD.POSITION) { + this.forEach((_) => { + _.target = _.current; + }); + } + this.mode = DynamicXD.POSITION; + this.speedMult = speedMult; + this.forEach((_) => { + if (_.name in steps) { + _.target = bound(_.target + steps[_.name], _.min, _.max); + } + }); + } + + /** + * Starts infinite movement + */ + roll(rolls, speedMult = 1) { + this.mode = DynamicXD.INFINITE; + this.speedMult = speedMult; + this.forEach((_) => { + if (_.name in rolls) { + _.target = rolls[_.name] ? -Infinity : Infinity; + } + else { + _.target = _.current; + } + }); + } + + /** + * Stops movement + */ + stop() { + this.mode = DynamicXD.STOP; + } + + /** + * Defines the current position and immediately stops movement + */ + setValue(values) { + this.mode = DynamicXD.STOP; + + const hasChanges = this.reduce((changes, _) => { + let changed = false; + if (_.name in values) { + const next = bound(values[_.name], _.min, _.max); + changed = next !== _.current; + _.current = next; + } + _.target = _.current; + return changes || changed; + }, false); + + if (hasChanges && this.fn) { + this.fn(this.current); + } + + return hasChanges; + } + + /** + * @package + */ + update(elapsed) { + const elapsedS = elapsed / 1000; + const acceleration = this.speed * this.speedMult * 2; + + // in position mode switch to stop mode when in the decceleration window + if (this.mode === DynamicXD.POSITION) { + const dstStop = this.currentSpeed * this.currentSpeed / (acceleration * 2); + const dstCurr = this.reduce((dst, _) => { + return dst + (_.target - _.current) * (_.target - _.current); + }, 0); + + if (dstCurr <= dstStop * dstStop) { // no Math.sqrt on dstCurr + this.mode = DynamicXD.STOP; + } + } + + // FIXME the speed should be different for each component (with sum = global speed) + // FIXME implement signed speed for smooth changes of direction + + // compute speed + const targetSpeed = this.mode === DynamicXD.STOP ? 0 : this.speed * this.speedMult; + if (this.currentSpeed < targetSpeed) { + this.currentSpeed = Math.min(targetSpeed, this.currentSpeed + elapsedS * acceleration); + } + else if (this.currentSpeed > targetSpeed) { + this.currentSpeed = Math.max(targetSpeed, this.currentSpeed - elapsedS * acceleration); + } + + if (this.currentSpeed) { + // compute position + const hasChanges = this.reduce((changes, _) => { + let next = null; + if (_.current > _.target) { + next = Math.max(_.target, _.current - this.currentSpeed * elapsedS); + } + else if (_.current < _.target) { + next = Math.min(_.target, _.current + this.currentSpeed * elapsedS); + } + + if (next !== null) { + next = bound(next, _.min, _.max); + if (next !== _.current) { + _.current = next; + return true; + } + } + + return changes; + }, false); + + // apply + if (hasChanges && this.fn) { + this.fn(this.current); + } + + return hasChanges; + } + + return false; + } + +} diff --git a/src/utils/MultiDynamic.js b/src/utils/MultiDynamic.js new file mode 100644 index 000000000..a92d8c706 --- /dev/null +++ b/src/utils/MultiDynamic.js @@ -0,0 +1,125 @@ +import { each } from './index'; + +/** + * @summary Wrapper for multiple {@link PSV.Dynamic} evolving together + * @memberOf PSV + * @package + */ +export class MultiDynamic { + + /** + * @type {Record} + * @readonly + */ + get current() { + const values = {}; + each(this.dynamics, (dynamic, name) => { + values[name] = dynamic.current; + }); + return values; + } + + /** + * @param {Record} dynamics + * @param {Function>} fn Callback function + */ + constructor(dynamics, fn) { + /** + * @type {Function>} + * @private + * @readonly + */ + this.fn = fn; + + /** + * @type {Record} + * @private + * @readonly + */ + this.dynamics = dynamics; + } + + /** + * Changes base speed + * @param {number} speed + */ + setSpeed(speed) { + each(this.dynamics, (d) => { + d.setSpeed(speed); + }); + } + + /** + * Defines the target positions + * @param {Record} positions + * @param {number} [speedMult=1] + */ + goto(positions, speedMult = 1) { + each(positions, (position, name) => { + this.dynamics[name].goto(position, speedMult); + }); + } + + /** + * Increase/decrease the target positions + * @param {Record} steps + * @param {number} [speedMult=1] + */ + step(steps, speedMult = 1) { + each(steps, (step, name) => { + this.dynamics[name].step(step, speedMult); + }); + } + + /** + * Starts infinite movements + * @param {Record} rolls + * @param {number} [speedMult=1] + */ + roll(rolls, speedMult = 1) { + each(rolls, (roll, name) => { + this.dynamics[name].roll(roll, speedMult); + }); + } + + /** + * Stops movements + */ + stop() { + each(this.dynamics, d => d.stop()); + } + + /** + * Defines the current positions and immediately stops movements + * @param {Record} values + */ + setValue(values) { + let hasUpdates = false; + each(values, (value, name) => { + hasUpdates |= this.dynamics[name].setValue(value); + }); + + if (hasUpdates && this.fn) { + this.fn(this.current); + } + + return hasUpdates; + } + + /** + * @package + */ + update(elapsed) { + let hasUpdates = false; + each(this.dynamics, (dynamic) => { + hasUpdates |= dynamic.update(elapsed); + }); + + if (hasUpdates && this.fn) { + this.fn(this.current); + } + + return hasUpdates; + } + +} diff --git a/src/utils/PressHandler.js b/src/utils/PressHandler.js new file mode 100644 index 000000000..e07184ab1 --- /dev/null +++ b/src/utils/PressHandler.js @@ -0,0 +1,43 @@ +/** + * @summary Helper for pressable things (buttons, keyboard) + * @description When the pressed thing goes up and was not pressed long enough, wait a bit more before execution + * @package + * @package + */ +export class PressHandler { + + constructor(delay = 200) { + this.delay = delay; + this.time = 0; + this.timeout = null; + } + + down() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = null; + } + + this.time = new Date().getTime(); + } + + up(cb) { + if (!this.time) { + return; + } + + const elapsed = new Date().getTime() - this.time; + if (elapsed < this.delay) { + this.timeout = setTimeout(() => { + cb(); + this.timeout = null; + this.time = 0; + }, this.delay); + } + else { + cb(); + this.time = 0; + } + } + +} diff --git a/src/utils/PressHandler.spec.js b/src/utils/PressHandler.spec.js new file mode 100644 index 000000000..5c6c649c9 --- /dev/null +++ b/src/utils/PressHandler.spec.js @@ -0,0 +1,32 @@ +import assert from 'assert'; +import { PressHandler } from './PressHandler'; + +describe('utils:PressHandler', () => { + it('should wait at least X ms before exec', (done) => { + const handler = new PressHandler(100); + + const start = new Date().getTime(); + + handler.down(); + handler.up(() => { + const elapsed = new Date().getTime() - start; + assert.ok(elapsed >= 100); + done(); + }); + }); + + it('should exec immediately if X ms already elapsed', (done) => { + const handler = new PressHandler(100); + + handler.down(); + + setTimeout(() => { + const start = new Date().getTime(); + handler.up(() => { + const elapsed = new Date().getTime() - start; + assert.ok(elapsed < 5); + done(); + }); + }, 200); + }); +});