From 27db0d9be6539c741987a3084a02696d5f50147c Mon Sep 17 00:00:00 2001 From: Johnny Martin Date: Wed, 8 Jul 2020 16:39:20 -0500 Subject: [PATCH 1/5] Inertial Panning without Tests --- README.md | 8 ++- src/MapInteraction.jsx | 120 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5e7b242..950243b 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ MapInteraction doesn't require any props. It will control its own internal state // contents appear at actual size, greater than 1 is zoomed, and between 0 and 1 is shrunken. scale: PropTypes.number, // The distance in pixels to translate the contents by. - translation: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }), + translation: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }), }), defaultValue: PropTypes.shape({ @@ -107,6 +107,12 @@ MapInteraction doesn't require any props. It will control its own internal state // changed via zooming, in order to keep the focal point beneath the cursor. This prop does not change the behavior of the `translation` prop. disablePan: PropTypes.bool, + // Stops the inertia or momentum of MapInteract after panning. + disableInertialPanning: PropTypes.bool, + + // Determines how fast the velocity is reduced from inertial panning. Must be between 0 and 1. Numbers closer to 0 will stop faster. + frictionCoef: PropTypes.number, + // Apply a limit to the translation in any direction in pixel values. The default is unbounded. translationBounds: PropTypes.shape({ xMin: PropTypes.number, xMax: PropTypes.number, yMin: PropTypes.number, yMax: PropTypes.number diff --git a/src/MapInteraction.jsx b/src/MapInteraction.jsx index ca726eb..22e770f 100644 --- a/src/MapInteraction.jsx +++ b/src/MapInteraction.jsx @@ -34,6 +34,8 @@ export class MapInteractionControlled extends Component { disableZoom: PropTypes.bool, disablePan: PropTypes.bool, + disableInertialPanning: PropTypes.bool, + frictionCoef: PropTypes.number, translationBounds: PropTypes.shape({ xMin: PropTypes.number, xMax: PropTypes.number, yMin: PropTypes.number, yMax: PropTypes.number }), @@ -56,7 +58,9 @@ export class MapInteractionControlled extends Component { showControls: false, translationBounds: {}, disableZoom: false, - disablePan: false + disablePan: false, + disableInertialPanning: false, + frictionCoef: 0.85 }; } @@ -69,6 +73,21 @@ export class MapInteractionControlled extends Component { this.startPointerInfo = undefined; + this.prevDragThrottled = false; + this.prevDragPos = { + time: undefined, + pos: { + x: 0, + y: 0 + } + }; + this.dragVelocity = { + x: 0, + y: 0, + } + this.inertialPanningLastRaf = undefined; + this.inertialPanningRafID = undefined; + this.onMouseDown = this.onMouseDown.bind(this); this.onTouchStart = this.onTouchStart.bind(this); @@ -79,6 +98,8 @@ export class MapInteractionControlled extends Component { this.onTouchEnd = this.onTouchEnd.bind(this); this.onWheel = this.onWheel.bind(this); + + this.inertialPanning = this.inertialPanning.bind(this); } componentDidMount() { @@ -134,19 +155,23 @@ export class MapInteractionControlled extends Component { onMouseDown(e) { e.preventDefault(); this.setPointerState([e]); + this.stopInertialPanning(); } onTouchStart(e) { e.preventDefault(); this.setPointerState(e.touches); + this.stopInertialPanning(); } onMouseUp(e) { this.setPointerState(); + this.triggerInertialPanning(); } onTouchEnd(e) { this.setPointerState(e.touches); + this.triggerInertialPanning(); } onMouseMove(e) { @@ -187,6 +212,21 @@ export class MapInteractionControlled extends Component { const shouldPreventTouchEndDefault = Math.abs(dragX) > 1 || Math.abs(dragY) > 1; + // Track drag positions every 150ms for inertial panning velocity calculations. + if (!this.props.disableInertialPanning && !this.prevDragThrottled) { + this.prevDragThrottled = true; + + this.prevDragPos = { + time: performance.now(), + pos: { + x: newTranslation.x, + y: newTranslation.y + } + }; + + setTimeout(() => this.prevDragThrottled = false, 150) + } + this.setState({ shouldPreventTouchEndDefault }, () => { @@ -245,6 +285,82 @@ export class MapInteractionControlled extends Component { }; } + calculateDragVelocity(timestamp, pos) { + const timeDiff = timestamp - this.prevDragPos.time; + const vX = (pos.x - this.prevDragPos.pos.x) / timeDiff; + const vY = (pos.y - this.prevDragPos.pos.y) / timeDiff; + + this.dragVelocity = {x: vX, y: vY}; + } + + stopInertialPanning() { + if (!this.disablePan && !this.props.disableInertialPanning) { + window.cancelAnimationFrame(this.inertialPanningRafID); + } + } + + triggerInertialPanning() { + if (!this.disablePan && !this.props.disableInertialPanning) { + + this.calculateDragVelocity(performance.now(), this.props.value.translation); + + // Clear previous drag position to avoid triggering again with the wrong values. + this.prevDragPos = { + time: undefined, + pos: { + x: 0, + y: 0 + } + }; + + this.prevDragThrottled = false; + this.inertialPanningLastRaf = undefined; + + this.inertialPanning(); + } + } + + inertialPanning(timestamp) { + if (!this.disablePan && !this.props.disableInertialPanning) { + if (typeof timestamp === "undefined") { + timestamp = performance.now(); + } + + // Stop running when velocity drops to a negligible number. + if (Math.abs(this.dragVelocity.x) > 0.01 || Math.abs(this.dragVelocity.y) > 0.01) { + // Elapsed time between rafs in ms. + let elapsedTime = 1; + + if (typeof this.inertialPanningLastRaf != "undefined") { + elapsedTime = timestamp - this.inertialPanningLastRaf; + } + + const translation = this.props.value.translation; + + const newTranslation = { + x: translation.x + this.dragVelocity.x*elapsedTime, + y: translation.y + this.dragVelocity.y*elapsedTime + } + + this.dragVelocity = { + x: this.dragVelocity.x * this.props.frictionCoef, + y: this.dragVelocity.y * this.props.frictionCoef + }; + + this.props.onChange({ + scale: this.props.value.scale, + translation: this.clampTranslation(newTranslation) + }); + + this.inertialPanningLastRaf = timestamp; + this.inertialPanningRafID = window.requestAnimationFrame(this.inertialPanning); + } else { + this.dragVelocity = {x: 0, y: 0}; + this.inertialPanningLastRaf = undefined; + } + } + } + translatedOrigin(translation = this.props.value.translation) { const clientOffset = this.getContainerBoundingClientRect(); return { @@ -443,6 +559,8 @@ class MapInteractionController extends Component { }), disableZoom: PropTypes.bool, disablePan: PropTypes.bool, + disableInertialPanning: PropTypes.bool, + frictionCoef: PropTypes.number, onChange: PropTypes.func, translationBounds: PropTypes.shape({ xMin: PropTypes.number, xMax: PropTypes.number, yMin: PropTypes.number, yMax: PropTypes.number From f3de2abec468810ea8138f61bf2812af39d8b377 Mon Sep 17 00:00:00 2001 From: Johnny Martin Date: Tue, 4 Aug 2020 12:42:17 -0500 Subject: [PATCH 2/5] Clamp prevDragPos to avoid bouncing off the walls of the bounds --- src/MapInteraction.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/MapInteraction.jsx b/src/MapInteraction.jsx index 22e770f..6ff97cb 100644 --- a/src/MapInteraction.jsx +++ b/src/MapInteraction.jsx @@ -287,10 +287,12 @@ export class MapInteractionControlled extends Component { calculateDragVelocity(timestamp, pos) { const timeDiff = timestamp - this.prevDragPos.time; - const vX = (pos.x - this.prevDragPos.pos.x) / timeDiff; - const vY = (pos.y - this.prevDragPos.pos.y) / timeDiff; + const clampedPrevDragPos = this.clampTranslation(this.prevDragPos.pos); + const vX = (pos.x - clampedPrevDragPos.x) / timeDiff; + const vY = (pos.y - clampedPrevDragPos.y) / timeDiff; this.dragVelocity = {x: vX, y: vY}; + } stopInertialPanning() { From ee085834874dc37d9f0d444ea4aea5cb48e3fdb8 Mon Sep 17 00:00:00 2001 From: Johnny Martin Date: Wed, 20 Jan 2021 15:04:47 -0600 Subject: [PATCH 3/5] Added onStopMoving event --- .gitignore | 1 + example/src/App.js | 2 ++ src/MapInteraction.jsx | 17 +++++++++++++++++ stories/index.stories.js | 6 ++++++ 4 files changed, 26 insertions(+) diff --git a/.gitignore b/.gitignore index df346d7..f11dfdc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist .DS_Store .nyc_output coverage +storybook-static \ No newline at end of file diff --git a/example/src/App.js b/example/src/App.js index 475d334..ce57ce2 100644 --- a/example/src/App.js +++ b/example/src/App.js @@ -32,6 +32,8 @@ class App extends Component { scale={scale} translation={translation} onChange={({ scale, translation }) => this.setState({ scale, translation })} + onStoppedMoving={() => console.log("stopped moving")} + disableInertialPanning={false} defaultScale={1} defaultTranslation={{ x: 0, y: 0 }} minScale={0.05} diff --git a/src/MapInteraction.jsx b/src/MapInteraction.jsx index 6ff97cb..a0ac5b9 100644 --- a/src/MapInteraction.jsx +++ b/src/MapInteraction.jsx @@ -31,6 +31,7 @@ export class MapInteractionControlled extends Component { translation: translationShape.isRequired, }).isRequired, onChange: PropTypes.func.isRequired, + onStoppedMoving: PropTypes.func, disableZoom: PropTypes.bool, disablePan: PropTypes.bool, @@ -167,11 +168,19 @@ export class MapInteractionControlled extends Component { onMouseUp(e) { this.setPointerState(); this.triggerInertialPanning(); + + if (this.props.disableInertialPanning) { + this.props.onStoppedMoving(); + } } onTouchEnd(e) { this.setPointerState(e.touches); this.triggerInertialPanning(); + + if (this.props.disableInertialPanning) { + this.props.onStoppedMoving(); + } } onMouseMove(e) { @@ -258,6 +267,12 @@ export class MapInteractionControlled extends Component { this.scaleFromPoint(newScale, mousePos); } + onStoppedMoving() { + if (typeof this.props.onStoppedMoving !== "undefined") { + this.props.onStoppedMoving(); + } + } + setPointerState(pointers) { if (!pointers || pointers.length === 0) { this.startPointerInfo = undefined; @@ -359,6 +374,7 @@ export class MapInteractionControlled extends Component { } else { this.dragVelocity = {x: 0, y: 0}; this.inertialPanningLastRaf = undefined; + this.onStoppedMoving(); } } } @@ -564,6 +580,7 @@ class MapInteractionController extends Component { disableInertialPanning: PropTypes.bool, frictionCoef: PropTypes.number, onChange: PropTypes.func, + onStoppedMoving: PropTypes.func, translationBounds: PropTypes.shape({ xMin: PropTypes.number, xMax: PropTypes.number, yMin: PropTypes.number, yMax: PropTypes.number }), diff --git a/stories/index.stories.js b/stories/index.stories.js index 53d5790..3735d75 100644 --- a/stories/index.stories.js +++ b/stories/index.stories.js @@ -36,6 +36,12 @@ storiesOf('MapInteractionCSS', module) onChange={(value) => { this.setState({ value }); }} + translationBounds={{ + xMin: -250, + xMax: 250, + yMin: -250, + yMax: 250 + }} showControls > From a0b9eca5cb305894cececc7861dd4122419291c8 Mon Sep 17 00:00:00 2001 From: Johnny Martin Date: Thu, 4 Mar 2021 16:52:15 -0600 Subject: [PATCH 4/5] Added extra wheel behaviors --- src/MapInteraction.jsx | 49 ++++++++++++++++++++++++++++------------ stories/index.stories.js | 37 ++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/MapInteraction.jsx b/src/MapInteraction.jsx index a0ac5b9..ab35109 100644 --- a/src/MapInteraction.jsx +++ b/src/MapInteraction.jsx @@ -48,7 +48,8 @@ export class MapInteractionControlled extends Component { btnClass: PropTypes.string, plusBtnClass: PropTypes.string, minusBtnClass: PropTypes.string, - controlsClass: PropTypes.string + controlsClass: PropTypes.string, + wheelBehavior: PropTypes.oneOf(['zoom', 'x-scroll', 'y-scroll']) }; } @@ -61,7 +62,8 @@ export class MapInteractionControlled extends Component { disableZoom: false, disablePan: false, disableInertialPanning: false, - frictionCoef: 0.85 + frictionCoef: 0.85, + wheelBehavior: 'zoom' }; } @@ -247,24 +249,40 @@ export class MapInteractionControlled extends Component { } onWheel(e) { - if (this.props.disableZoom) { - return; - } - e.preventDefault(); e.stopPropagation(); - const scaleChange = 2 ** (e.deltaY * 0.002); + if (this.props.wheelBehavior === 'zoom') { + if (this.props.disableZoom) { + return; + } + + const scaleChange = 2 ** (e.deltaY * 0.002); - const newScale = clamp( - this.props.minScale, - this.props.value.scale + (1 - scaleChange), - this.props.maxScale - ); + const newScale = clamp( + this.props.minScale, + this.props.value.scale + (1 - scaleChange), + this.props.maxScale + ); - const mousePos = this.clientPosToTranslatedPos({ x: e.clientX, y: e.clientY }); + const mousePos = this.clientPosToTranslatedPos({ x: e.clientX, y: e.clientY }); + + this.scaleFromPoint(newScale, mousePos); + } else { + const translation = this.props.value.translation; - this.scaleFromPoint(newScale, mousePos); + const scollChange = e.deltaY; + + const newTranslation = { + x: translation.x + (this.props.wheelBehavior === 'x-scroll' ? (1 - scollChange) : 0), + y: translation.y + (this.props.wheelBehavior === 'y-scroll' ? (1 - scollChange) : 0) + }; + + this.props.onChange({ + scale: this.props.value.scale, + translation: this.clampTranslation(newTranslation) + }) + } } onStoppedMoving() { @@ -592,7 +610,8 @@ class MapInteractionController extends Component { btnClass: PropTypes.string, plusBtnClass: PropTypes.string, minusBtnClass: PropTypes.string, - controlsClass: PropTypes.string + controlsClass: PropTypes.string, + wheelBehavior: PropTypes.oneOf(['zoom', 'x-scroll', 'y-scroll']), }; } diff --git a/stories/index.stories.js b/stories/index.stories.js index 3735d75..122fab8 100644 --- a/stories/index.stories.js +++ b/stories/index.stories.js @@ -172,3 +172,40 @@ storiesOf('MapInteractionCSS', module) ) }) + .add('Wheel Scroll Y', () => { + class Controller extends Component { + constructor(props) { + super(props); + this.state = { + value: { + scale: 1, translation: { x: 0, y: 0 } + } + }; + } + + render() { + return ( +
+ { + this.setState({ value }); + }} + translationBounds={{ + xMin: -250, + xMax: 250, + yMin: -250, + yMax: 250 + }} + showControls + scrollBehavior={'y-scroll'} + > + + +
+ ); + } + } + + return ; + }) \ No newline at end of file From f73d4a3e5afd4147d738cb526526be3e7bb03e34 Mon Sep 17 00:00:00 2001 From: Johnny Martin Date: Thu, 4 Mar 2021 16:55:37 -0600 Subject: [PATCH 5/5] Incremented version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df313de..6645ccf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-map-interaction", - "version": "2.0.0", + "version": "2.0.1", "description": "'Add map like zooming and dragging to any element'", "main": "dist/react-map-interaction.js", "scripts": {