Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Two Finger Pan Support #323

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,18 @@ assert(instance.getMaxZoom() === 1);
assert(instance.getMinZoom() === 0.1);
```

## Two Finger Pan

You can enable panning while pinch-zooming, similar to Google/Apple Maps, by passing the optional enableTwoFingerPan argument:

``` js
panzoom(element, {
enableTwoFingerPan: true
});
```

Note: When enabled, `transformOrigin` is ignored during pinch-zooming.

## Disable Smooth Scroll

You can disable smooth scroll, by passing optional `smoothScroll` argument:
Expand Down
770 changes: 770 additions & 0 deletions demo/two-finger-pan.html

Large diffs are not rendered by default.

166 changes: 103 additions & 63 deletions dist/panzoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function createPanZoom(domElement, options) {
var speed = typeof options.zoomSpeed === 'number' ? options.zoomSpeed : defaultZoomSpeed;
var transformOrigin = parseTransformOrigin(options.transformOrigin);
var textSelection = options.enableTextSelection ? fakeTextSelectorInterceptor : domTextSelectionInterceptor;
var enableTwoFingerPan = !!options.enableTwoFingerPan;

validateBounds(bounds);

Expand Down Expand Up @@ -108,7 +109,7 @@ function createPanZoom(domElement, options) {
} else {
// otherwise we use forward smoothScroll settings to kinetic API
// which makes scroll smoothing.
smoothScroll = kinetic(getPoint, scroll, options.smoothScroll);
smoothScroll = kinetic(getPoint, getTarget, scroll, options.smoothScroll);
}

var moveByAnimation;
Expand Down Expand Up @@ -287,24 +288,31 @@ function createPanZoom(domElement, options) {
}

function getPoint() {
return {
x: mouseX,
y: mouseY,
};
}

function getTarget() {
return {
x: transform.x,
y: transform.y
};
}

function moveTo(x, y) {
function moveTo(x, y, dirty = true) {
transform.x = x;
transform.y = y;

keepTransformInsideBounds();

triggerEvent('pan');
makeDirty();
if (dirty) makeDirty();
}

function moveBy(dx, dy) {
moveTo(transform.x + dx, transform.y + dy);
function moveBy(dx, dy, dirty = true) {
moveTo(transform.x + dx, transform.y + dy, dirty);
}

function keepTransformInsideBounds() {
Expand Down Expand Up @@ -595,12 +603,9 @@ function createPanZoom(domElement, options) {
clearPendingClickEventTimeout();

if (e.touches.length === 1) {
return handleSingleFingerTouch(e, e.touches[0]);
return handleSingleFingerTouch(e);
} else if (e.touches.length === 2) {
// handleTouchMove() will care about pinch zoom.
pinchZoomLength = getPinchZoomLength(e.touches[0], e.touches[1]);
multiTouch = true;
startTouchListenerIfNeeded();
return handleTwoFingerTouch(e);
}
}

Expand Down Expand Up @@ -645,6 +650,20 @@ function createPanZoom(domElement, options) {
startTouchListenerIfNeeded();
}

function handleTwoFingerTouch(e) {
// handleTouchMove() will care about pan and pinch zoom.
pinchZoomLength = getPinchZoomLength(e.touches[0], e.touches[1]);

// pan init
var offset = calcMidOffset(e.touches[0], e.touches[1]);
var point = transformToScreen(offset.x, offset.y);
mouseX = point.x;
mouseY = point.y;

multiTouch = true;
startTouchListenerIfNeeded();
}

function startTouchListenerIfNeeded() {
if (touchInProgress) {
// no need to do anything, as we already listen to events;
Expand All @@ -658,50 +677,61 @@ function createPanZoom(domElement, options) {
}

function handleTouchMove(e) {
if (e.touches.length === 1) {
if (e.touches.length > 2) return;
if (multiTouch && e.touches.length === 1) return;
if (!multiTouch && e.touches.length === 2) return;

var offset = e.touches.length === 1
? getOffsetXY(e.touches[0])
: calcMidOffset(e.touches[0], e.touches[1]);

if (!multiTouch) {
handleTouchMovePan(e, offset);
} else {
handleTouchMoveZoom(e, offset);
handleTouchMovePan(e, offset);
}
}

function handleTouchMovePan(e, offset) {
if (multiTouch && !enableTwoFingerPan) {
e.stopPropagation();
var touch = e.touches[0];
return;
}

var point = transformToScreen(offset.x, offset.y);
var dx = point.x - mouseX;
var dy = point.y - mouseY;

var offset = getOffsetXY(touch);
var point = transformToScreen(offset.x, offset.y);
if (dx !== 0 && dy !== 0) {
triggerPanStart();
}

var dx = point.x - mouseX;
var dy = point.y - mouseY;
mouseX = point.x;
mouseY = point.y;

if (dx !== 0 && dy !== 0) {
triggerPanStart();
}
mouseX = point.x;
mouseY = point.y;
internalMoveBy(dx, dy);
} else if (e.touches.length === 2) {
// it's a zoom, let's find direction
multiTouch = true;
var t1 = e.touches[0];
var t2 = e.touches[1];
var currentPinchLength = getPinchZoomLength(t1, t2);

// since the zoom speed is always based on distance from 1, we need to apply
// pinch speed only on that distance from 1:
var scaleMultiplier =
1 + (currentPinchLength / pinchZoomLength - 1) * pinchSpeed;

var firstTouchPoint = getOffsetXY(t1);
var secondTouchPoint = getOffsetXY(t2);
mouseX = (firstTouchPoint.x + secondTouchPoint.x) / 2;
mouseY = (firstTouchPoint.y + secondTouchPoint.y) / 2;
if (transformOrigin) {
var offset = getTransformOriginOffset();
mouseX = offset.x;
mouseY = offset.y;
}
moveBy(dx, dy, !multiTouch);
e.stopPropagation();
}

publicZoomTo(mouseX, mouseY, scaleMultiplier);
function handleTouchMoveZoom(e, offset) {
var currentPinchLength = getPinchZoomLength(e.touches[0], e.touches[1]);
// since the zoom speed is always based on distance from 1, we need to apply
// pinch speed only on that distance from 1:
var scaleMultiplier =
1 + (currentPinchLength / pinchZoomLength - 1) * pinchSpeed;

pinchZoomLength = currentPinchLength;
e.stopPropagation();
e.preventDefault();
if (transformOrigin && !enableTwoFingerPan) {
offset = getTransformOriginOffset();
mouseX = offset.x;
mouseY = offset.y;
}

publicZoomTo(offset.x, offset.y, scaleMultiplier);

pinchZoomLength = currentPinchLength;

e.preventDefault();
}

function clearPendingClickEventTimeout() {
Expand Down Expand Up @@ -730,10 +760,13 @@ function createPanZoom(domElement, options) {
function handleTouchEnd(e) {
clearPendingClickEventTimeout();
if (e.touches.length > 0) {
var offset = getOffsetXY(e.touches[0]);
var point = transformToScreen(offset.x, offset.y);
mouseX = point.x;
mouseY = point.y;
// prevents conflict with smooth scroll and pinch zoom when two finger pan is enabled.
if (!multiTouch) {
var offset = getOffsetXY(e.touches[0]);
var point = transformToScreen(offset.x, offset.y);
mouseX = point.x;
mouseY = point.y;
}
} else {
var now = new Date();
if (now - lastTouchEndTime < doubleTapSpeedInMS) {
Expand Down Expand Up @@ -762,6 +795,14 @@ function createPanZoom(domElement, options) {
return Math.sqrt(dx * dx + dy * dy);
}

function calcMidOffset(t1, t2) {
const mid = (num1, num2) => (num1 + num2) / 2;
var x = mid(t1.clientX, t2.clientX);
var y = mid(t1.clientY, t2.clientY);

return getOffsetXY({clientX: x, clientY: y});
}

function onDoubleClick(e) {
beforeDoubleClick(e);
var offset = getOffsetXY(e);
Expand Down Expand Up @@ -920,7 +961,6 @@ function createPanZoom(domElement, options) {
}

function publicZoomTo(clientX, clientY, scaleMultiplier) {
smoothScroll.cancel();
cancelZoomAnimation();
return zoomByRatio(clientX, clientY, scaleMultiplier);
}
Expand Down Expand Up @@ -948,8 +988,8 @@ function createPanZoom(domElement, options) {

function triggerPanEnd() {
if (panstartFired) {
// we should never run smooth scrolling if it was multiTouch (pinch zoom animation):
if (!multiTouch) smoothScroll.stop();
// we should never run smooth scrolling if it was multiTouch and enableTwoFingerPan disabled:
if (!multiTouch || enableTwoFingerPan) smoothScroll.stop();
triggerEvent('panend');
}
}
Expand Down Expand Up @@ -1106,7 +1146,7 @@ autoRun();
*/
module.exports = kinetic;

function kinetic(getPoint, scroll, settings) {
function kinetic(getPoint, getTarget, scroll, settings) {
if (typeof settings !== 'object') {
// setting could come as boolean, we should ignore it, and use an object.
settings = {};
Expand Down Expand Up @@ -1178,10 +1218,10 @@ function kinetic(getPoint, scroll, settings) {
cancelAnimationFrame(ticker);
cancelAnimationFrame(raf);

var currentPoint = getPoint();
var target = getTarget();

targetX = currentPoint.x;
targetY = currentPoint.y;
targetX = target.x;
targetY = target.y;
timestamp = Date.now();

if (vx < -minVelocity || vx > minVelocity) {
Expand Down Expand Up @@ -1327,12 +1367,12 @@ function makeSvgController(svgElement, options) {
}

function getBBox() {
var bbox = svgElement.getBBox();
var boundingBox = svgElement.getBBox();
return {
left: bbox.x,
top: bbox.y,
width: bbox.width,
height: bbox.height,
left: boundingBox.x,
top: boundingBox.y,
width: boundingBox.width,
height: boundingBox.height,
};
}

Expand Down
2 changes: 1 addition & 1 deletion dist/panzoom.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ declare module "panzoom" {
enableTextSelection?: boolean;
disableKeyboardInteraction?: boolean;
transformOrigin?: TransformOrigin;
enableTwoFingerPan?: boolean;
}

export interface PanZoom {
Expand Down
Loading