Skip to content

Commit

Permalink
implement more intuitive zoom gesture behaviour 馃挒 (#284)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pascal-So committed May 9, 2024
1 parent 6a75c4b commit 3688890
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/cool-clouds-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zoom-image/core": patch
---

Make pinch zooming behaviour more intuitive
52 changes: 26 additions & 26 deletions packages/core/src/createZoomImageWheel.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<number, PointerPosition>()

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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 })
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(predicateFn: () => boolean, fn: (arg: T) => void) {
return (arg: T) => {
if (predicateFn()) {
Expand Down

0 comments on commit 3688890

Please sign in to comment.