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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent jump when zooming out with double tap #292

Merged
merged 2 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/kind-impalas-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zoom-image/core": patch
---

Prevent jump when zooming out with double tap
112 changes: 59 additions & 53 deletions packages/core/src/createZoomImageWheel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,87 +237,93 @@ export function createZoomImageWheel(container: HTMLElement, options: ZoomImageW
}
}

// These variables are used for zooming on double tap
let touchTimer: NodeJS.Timeout | null = null
let startTimestamp = 0
let currentValue = 0
const endValue = 100
let zoomDirection: "in" | "out" = "in"
let x = 0
let y = 0
const durationBetweenTap = 300
const animationState = {
startTimestamp: null as DOMHighResTimeStamp | null,
// the state at the start of the zoom animation
start: { x: 0, y: 0, zoom: 0 },
// the target state at the end of the zoom animation
target: { x: 0, y: 0, zoom: 0 },
}

function animateZoom(touchCoordinate: { x: number; y: number }) {
// the `touchCoordinate` should be relative to the container

function animateZoom(timestamp: number) {
const currentState = store.getState()
const containerRect = container.getBoundingClientRect()
const zoomPointX = x - containerRect.left
const zoomPointY = y - containerRect.top
const isDimensionSwitched = checkDimensionSwitched()
const zoomX = isDimensionSwitched ? currentState.currentPositionY : currentState.currentPositionX
const zoomY = isDimensionSwitched ? currentState.currentPositionX : currentState.currentPositionY
const zoomTargetX = (zoomPointX - zoomX) / currentState.currentZoom
const zoomTargetY = (zoomPointY - zoomY) / currentState.currentZoom

if (!startTimestamp) {
startTimestamp = timestamp
zoomDirection = currentState.currentZoom > 1 ? "out" : "in"
animationState.startTimestamp = null
animationState.start = {
x: currentState.currentPositionX,
y: currentState.currentPositionY,
zoom: currentState.currentZoom,
}

const progress = timestamp - startTimestamp
currentValue = Math.min((progress / finalOptions.dblTapAnimationDuration) * endValue, endValue)
if (currentState.currentZoom > 1) {
animationState.target = {
x: 0,
y: 0,
zoom: 1,
}
} else {
animationState.target = {
zoom: finalOptions.maxZoom,
x: touchCoordinate.x * (1 - finalOptions.maxZoom),
y: touchCoordinate.y * (1 - finalOptions.maxZoom),
}
}

if (zoomDirection === "in") {
const newCurrentZoom = clamp(1 + (finalOptions.maxZoom - 1) * (currentValue / 100), 1, finalOptions.maxZoom)
function lerp(a: number, b: number, t: number): number {
return a * (1 - t) + b * t
}
Comment on lines +274 to +276
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh man, thanks so much! I really miss Math back in university and high school 鉂わ笍 What I do nowadays in the industry as FrontEnd engineer has almost nothing to do with numbers, mostly logic 馃檲 I should start spending more time on this beautiful stuff 馃槏


store.setState({
currentZoom: newCurrentZoom,
currentPositionX: calculatePositionX(-zoomTargetX * newCurrentZoom + zoomPointX, newCurrentZoom),
currentPositionY: calculatePositionY(-zoomTargetY * newCurrentZoom + zoomPointY, newCurrentZoom),
})
function frame(timestamp: DOMHighResTimeStamp) {
if (animationState.startTimestamp === null) {
animationState.startTimestamp = timestamp
}

updateZoom()
}
// interpolation parameter that linearly goes from 0 to 1 during the animation
let t = (timestamp - animationState.startTimestamp) / finalOptions.dblTapAnimationDuration
if (t > 1) {
t = 1
}

if (zoomDirection === "out") {
const newCurrentZoom = clamp(
1 + (finalOptions.maxZoom - 1) - (finalOptions.maxZoom - 1) * (currentValue / 100),
1,
finalOptions.maxZoom,
)
store.setState({
currentZoom: newCurrentZoom,
currentPositionX: calculatePositionX(-zoomPointX * newCurrentZoom + zoomPointX, newCurrentZoom),
currentPositionY: calculatePositionY(-zoomPointY * newCurrentZoom + zoomPointY, newCurrentZoom),
currentPositionX: lerp(animationState.start.x, animationState.target.x, t),
currentPositionY: lerp(animationState.start.y, animationState.target.y, t),
currentZoom: lerp(animationState.start.zoom, animationState.target.zoom, t),
})

updateZoom()
}

if (progress < finalOptions.dblTapAnimationDuration) {
requestAnimationFrame(animateZoom)
} else {
currentValue = 0
startTimestamp = 0
if (t < 1) {
requestAnimationFrame(frame)
}
}

requestAnimationFrame(frame)
}

// These variables are used for zooming on double tap
let touchTimer: NodeJS.Timeout | null = null
const durationBetweenTap = 300

function _handleTouchStart(event: TouchEvent) {
event.preventDefault()
if (event.touches.length > 1) {
return
}

x = event.touches[0].clientX
y = event.touches[0].clientY

if (touchTimer === null) {
touchTimer = setTimeout(() => {
touchTimer = null
}, durationBetweenTap)
} else {
clearTimeout(touchTimer)
touchTimer = null
requestAnimationFrame(animateZoom)

const rect = container.getBoundingClientRect()
const touch = event.touches[0]
animateZoom({
x: touch.clientX - rect.left,
y: touch.clientY - rect.top,
})
return
}
}
Expand Down