Add touch support for pan and pinch-zoom on iOS

This commit is contained in:
Danila Poyarkov 2026-03-01 20:13:21 +03:00
parent c3844db2bd
commit 575914b194
4 changed files with 106 additions and 2 deletions

View file

@ -6,7 +6,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
<link rel="icon" type="image/png" sizes="128x128" href="/favicon-128.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>OpenPencil</title>
<style>
body { margin: 0; background: #1e1e1e; }

View file

@ -41,7 +41,7 @@ const cursor = computed(() => {
<template>
<CanvasContextMenu>
<div class="canvas-area relative flex-1 min-w-0 min-h-0 overflow-hidden">
<canvas ref="canvasRef" :style="{ cursor }" class="block size-full" />
<canvas ref="canvasRef" :style="{ cursor }" class="block size-full touch-none" />
</div>
</CanvasContextMenu>
</template>

View file

@ -1046,6 +1046,101 @@ export function useCanvasInput(
})
}
// Touch support for iOS/mobile: single-finger pan, two-finger pinch-zoom
let activeTouches: Touch[] = []
let pinchStartDist = 0
let pinchStartZoom = 0
let pinchMidX = 0
let pinchMidY = 0
function touchDist(a: Touch, b: Touch) {
return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY)
}
function onTouchStart(e: TouchEvent) {
e.preventDefault()
activeTouches = Array.from(e.touches)
const canvas = canvasRef.value
if (!canvas) return
if (activeTouches.length === 2) {
drag.value = null
const [a, b] = activeTouches
pinchStartDist = touchDist(a, b)
pinchStartZoom = store.state.zoom
const rect = canvas.getBoundingClientRect()
pinchMidX = (a.clientX + b.clientX) / 2 - rect.left
pinchMidY = (a.clientY + b.clientY) / 2 - rect.top
} else if (activeTouches.length === 1) {
const t = activeTouches[0]
drag.value = {
type: 'pan',
startScreenX: t.clientX,
startScreenY: t.clientY,
startPanX: store.state.panX,
startPanY: store.state.panY
}
}
}
function onTouchMove(e: TouchEvent) {
e.preventDefault()
activeTouches = Array.from(e.touches)
const canvas = canvasRef.value
if (!canvas) return
if (activeTouches.length === 2) {
const [a, b] = activeTouches
const rect = canvas.getBoundingClientRect()
const newMidX = (a.clientX + b.clientX) / 2 - rect.left
const newMidY = (a.clientY + b.clientY) / 2 - rect.top
const newDist = touchDist(a, b)
if (pinchStartDist > 0) {
const scale = newDist / pinchStartDist
const newZoom = Math.max(0.02, Math.min(256, pinchStartZoom * scale))
const zoomRatio = newZoom / store.state.zoom
const panDx = newMidX - pinchMidX
const panDy = newMidY - pinchMidY
store.state.panX = pinchMidX - (pinchMidX - store.state.panX) * zoomRatio + panDx
store.state.panY = pinchMidY - (pinchMidY - store.state.panY) * zoomRatio + panDy
store.state.zoom = newZoom
}
pinchMidX = newMidX
pinchMidY = newMidY
store.requestRepaint()
} else if (activeTouches.length === 1 && drag.value?.type === 'pan') {
const t = activeTouches[0]
const d = drag.value
store.state.panX = d.startPanX + (t.clientX - d.startScreenX)
store.state.panY = d.startPanY + (t.clientY - d.startScreenY)
store.requestRepaint()
}
}
function onTouchEnd(e: TouchEvent) {
e.preventDefault()
activeTouches = Array.from(e.touches)
if (activeTouches.length === 0) {
drag.value = null
pinchStartDist = 0
} else if (activeTouches.length === 1) {
const t = activeTouches[0]
drag.value = {
type: 'pan',
startScreenX: t.clientX,
startScreenY: t.clientY,
startPanX: store.state.panX,
startPanY: store.state.panY
}
pinchStartDist = 0
}
}
useEventListener(canvasRef, 'dblclick', onDblClick)
useEventListener(canvasRef, 'mousedown', onMouseDown)
useEventListener(canvasRef, 'mousemove', onMouseMove)
@ -1055,6 +1150,10 @@ export function useCanvasInput(
store.setHoveredNode(null)
})
useEventListener(canvasRef, 'wheel', onWheel, { passive: false })
useEventListener(canvasRef, 'touchstart', onTouchStart, { passive: false })
useEventListener(canvasRef, 'touchmove', onTouchMove, { passive: false })
useEventListener(canvasRef, 'touchend', onTouchEnd, { passive: false })
useEventListener(canvasRef, 'touchcancel', onTouchEnd, { passive: false })
return {
drag,

View file

@ -37,6 +37,11 @@ useEventListener(
{ passive: false }
)
// Prevent Safari's native pinch-zoom gesture on the page
useEventListener(document, 'gesturestart', (e: Event) => e.preventDefault(), { passive: false })
useEventListener(document, 'gesturechange', (e: Event) => e.preventDefault(), { passive: false })
useEventListener(document, 'gestureend', (e: Event) => e.preventDefault(), { passive: false })
const params = useUrlSearchParams('history')
const showChrome = !('no-chrome' in params)
if (!('test' in params)) {