touch-action: none을 주면 브라우저 제스처를 직접 만들어야 한다
그리기 캔버스에 touch-action: none을 주면 손가락 스크롤까지 막힌다. 한 손가락은 그리기, 두 손가락은 스크롤로 직접 나눈다.
그리기 캔버스에서 손가락으로 선을 그을 때 페이지가 같이 스크롤되지 않게 touch-action: none을 줬더니, 이번에는 캔버스 위에서 스크롤을 아예 할 수 없다. 캔버스가 화면을 덮는 모바일에서는 페이지에 갇힌다.
touch-action: none은 그 요소 위에서 브라우저의 모든 터치 제스처(스크롤, 핀치 확대)를 끈다. 그리기만 살리고 스크롤만 남기는 선택지는 없다. 끈 제스처가 필요하면 직접 만들어야 한다.
포인터를 pointerId별로 추적한다. 손가락 하나면 그리기, 두 번째 손가락이 닿으면 진행 중인 획을 취소하고 두 점의 가운데가 움직인 만큼 스크롤한다. 펜(pointerType === 'pen')은 하나여도 항상 그리기로 친다.
const pointers = new Map<number, { x: number; y: number }>();
let lastMid: { x: number; y: number } | null = null;
const mid = () => {
const [a, b] = [...pointers.values()];
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
};
canvas.addEventListener('pointerdown', (e) => {
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (e.pointerType === 'pen' || pointers.size === 1) startStroke(e);
else if (pointers.size === 2) {
cancelStroke();
lastMid = mid();
}
});
canvas.addEventListener('pointermove', (e) => {
if (!pointers.has(e.pointerId)) return;
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
if (pointers.size === 2 && lastMid) {
const m = mid();
scroller.scrollBy(lastMid.x - m.x, lastMid.y - m.y);
lastMid = m;
} else {
extendStroke(e);
}
});
const release = (e: PointerEvent) => {
pointers.delete(e.pointerId);
if (pointers.size < 2) lastMid = null;
if (pointers.size === 0) endStroke(e);
};
canvas.addEventListener('pointerup', release);
canvas.addEventListener('pointercancel', release);
핀치 확대도 필요하면 같은 방식으로, 두 점 사이 거리의 비율로 배율을 바꾼다.