From 36888903da995792ccdb995dd54e01806ee0e872 Mon Sep 17 00:00:00 2001 From: Pascal Sommer Date: Thu, 9 May 2024 19:53:39 +0200 Subject: [PATCH] =?UTF-8?q?implement=20more=20intuitive=20zoom=20gesture?= =?UTF-8?q?=20behaviour=20=F0=9F=92=9E=20(#284)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/cool-clouds-shout.md | 5 +++ packages/core/src/createZoomImageWheel.ts | 52 +++++++++++------------ packages/core/src/utils.ts | 28 ++++++++++++ 3 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 .changeset/cool-clouds-shout.md diff --git a/.changeset/cool-clouds-shout.md b/.changeset/cool-clouds-shout.md new file mode 100644 index 00000000..0f5d647e --- /dev/null +++ b/.changeset/cool-clouds-shout.md @@ -0,0 +1,5 @@ +--- +"@zoom-image/core": patch +--- + +Make pinch zooming behaviour more intuitive diff --git a/packages/core/src/createZoomImageWheel.ts b/packages/core/src/createZoomImageWheel.ts index 5a0c1a1d..a1eec24a 100644 --- a/packages/core/src/createZoomImageWheel.ts +++ b/packages/core/src/createZoomImageWheel.ts @@ -1,6 +1,6 @@ import { createStore } from "@namnode/store" import type { PointerPosition } from "./utils" -import { clamp, disableScroll, enableScroll, getPointersCenter, getSourceImage, makeMaybeCallFunction } from "./utils" +import { clamp, computeZoomGesture, disableScroll, enableScroll, getSourceImage, makeMaybeCallFunction } from "./utils" export type ZoomImageWheelOptions = { maxZoom?: number @@ -64,7 +64,8 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW return newPositionY } - let prevDistance = -1 + // last pair of coordinates of a touch with two fingers + let prevTwoPositions: [PointerPosition, PointerPosition] | null = null let enabledScroll = true const pointerMap = new Map() @@ -200,28 +201,6 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW } } - if (pointerMap.size === 2) { - const pointersIterator = pointerMap.values() - const first = pointersIterator.next().value as PointerPosition - const second = pointersIterator.next().value as PointerPosition - const curDistance = Math.sqrt(Math.pow(first.x - second.x, 2) + Math.pow(first.y - second.y, 2)) - const { x, y } = getPointersCenter(first, second) - if (prevDistance > 0) { - if (curDistance > prevDistance) { - // The distance between the two pointers has increased - processZoomWheel({ delta: ZOOM_DELTA, x, y }) - } - if (curDistance < prevDistance) { - // The distance between the two pointers has decreased - processZoomWheel({ delta: -ZOOM_DELTA, x, y }) - } - } - // Store the distance for the next move event - prevDistance = curDistance - updateZoom() - return - } - if (pointerMap.size === 1) { const { currentZoom, currentRotation } = store.getState() const isDimensionSwitched = checkDimensionSwitched() @@ -343,6 +322,25 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW } } + function _handleTouchMove(event: TouchEvent) { + event.preventDefault() + if (event.touches.length === 2) { + const currentTwoPositions = [...event.touches].map((t) => ({ x: t.clientX, y: t.clientY })) as [ + PointerPosition, + PointerPosition, + ] + + if (prevTwoPositions !== null) { + const { scale, center } = computeZoomGesture(prevTwoPositions, currentTwoPositions) + processZoomWheel({ delta: Math.log(scale) / finalOptions.wheelZoomRatio, ...center }) + } + // Store the current two pointer positions for the next move event + prevTwoPositions = currentTwoPositions + updateZoom() + return + } + } + function _handlePointerDown(event: PointerEvent) { event.preventDefault() if (pointerMap.size === 2) { @@ -372,7 +370,7 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW // Reset the distance as soon as one of the pointers is released if (pointerMap.size < 2) { - prevDistance = -1 + prevTwoPositions = null } if (pointerMap.size === 0 && !enabledScroll) { @@ -396,7 +394,7 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW function _handlePointerLeave(event: PointerEvent) { event.preventDefault() pointerMap.delete(event.pointerId) - prevDistance = -1 + prevTwoPositions = null if (!enabledScroll) { enableScroll() enabledScroll = true @@ -413,11 +411,13 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW const handlePointerMove = makeMaybeCallFunction(checkZoomEnabled, _handlePointerMove) const handlePointerUp = makeMaybeCallFunction(checkZoomEnabled, _handlePointerUp) const handleTouchStart = makeMaybeCallFunction(checkZoomEnabled, _handleTouchStart) + const handleTouchMove = makeMaybeCallFunction(checkZoomEnabled, _handleTouchMove) const controller = new AbortController() const { signal } = controller container.addEventListener("wheel", handleWheel, { signal }) container.addEventListener("touchstart", handleTouchStart, { signal }) + container.addEventListener("touchmove", handleTouchMove, { signal }) container.addEventListener("pointerdown", handlePointerDown, { signal }) container.addEventListener("pointerleave", handlePointerLeave, { signal }) container.addEventListener("pointermove", handlePointerMove, { signal }) diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 3b2fb59a..e20bfca7 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -56,6 +56,34 @@ export function getPointersCenter(first: PointerPosition, second: PointerPositio } } +// Given the previous and current positions of two touch inputs, compute the zoom +// factor and the origin of the zoom gesture. +export function computeZoomGesture(prev: [PointerPosition, PointerPosition], curr: [PointerPosition, PointerPosition]) { + const prevCenter = getPointersCenter(prev[0], prev[1]) + const currCenter = getPointersCenter(curr[0], curr[1]) + const centerDist = { x: currCenter.x - prevCenter.x, y: currCenter.y - prevCenter.y } + + const prevDistance = Math.hypot(prev[0].x - prev[1].x, prev[0].y - prev[1].y) + const currDistance = Math.hypot(curr[0].x - curr[1].x, curr[0].y - curr[1].y) + let scale = currDistance / prevDistance + + // avoid division by zero + const eps = 0.00001 + if (Math.abs(scale - 1) < eps) { + scale = 1 + eps + } + + return { + scale, + center: { + // We shift the zoom center away such that the translation part of the gesture + // is also captured by the zoom operation. + x: prevCenter.x + centerDist.x / (1 - scale), + y: prevCenter.y + centerDist.y / (1 - scale), + }, + } +} + export function makeMaybeCallFunction(predicateFn: () => boolean, fn: (arg: T) => void) { return (arg: T) => { if (predicateFn()) {