Pointer Events와 드래그
이번에는 입력입니다. 사용자가 마우스를 누르고, 움직이고, 떼는 그 평범한 동작 말입니다.
편집 도구의 대부분은 이 작은 흐름 위에 올라갑니다.
pointerdown -> pointermove -> pointerup
간단해 보이죠. 그런데 제대로 만들지 않으면 드래그 중 포인터가 요소 밖으로 나갔을 때 이벤트가 끊기고, 도구 상태가 멈추고, 사용자는 “방금 뭐였지?” 하게 됩니다. 그래서 pointer lifecycle을 깔끔하게 잡아야 합니다.
드래그는 작은 상태 기계다
드래그는 그냥 이벤트 세 개가 아니라 상태 전환입니다.
idle -> pressing -> dragging -> committing -> idle
pointerdown에서는 시작점을 저장합니다. pointermove에서는 현재점과 시작점의 차이를 계산합니다. pointerup에서는 최종 값을 모델에 반영합니다.
dragDelta = currentClient - startClient
worldDelta = dragDelta / zoom
화면에서 20px 움직였다고 해서 world에서도 항상 20px 움직인 것은 아닙니다. zoom이 2라면 world 기준 이동량은 10입니다. 입력은 screen 좌표로 들어오고, 편집 모델은 world 좌표를 원합니다. 여기서도 좌표 변환이 슬쩍 웃고 있습니다.
pointer capture를 쓰자
드래그 중 포인터가 요소 밖으로 나가도 이벤트를 계속 받고 싶다면 setPointerCapture()를 씁니다.
event.currentTarget.setPointerCapture(event.pointerId);
이걸 해두면 사용자가 빠르게 움직여도 pointermove, pointerup을 안정적으로 받을 수 있습니다. resize handle이나 rotation handle처럼 작은 UI를 잡고 움직일 때 특히 중요합니다.
click과 drag는 구분해야 한다
pointerdown 직후 아주 조금 움직였다고 바로 drag로 처리하면 클릭이 자꾸 드래그로 오해받습니다. 그래서 보통 threshold를 둡니다.
if length(current - start) > 3px:
dragging = true
터치, 펜, 마우스는 입력 특성이 조금씩 다릅니다. pressure, pointerType, wheel delta 같은 값은 장치마다 다르게 들어올 수 있으니 도구 레벨에서는 정규화해서 쓰는 편이 좋습니다.
취소도 상태다
pointercancel, blur, Escape 처리를 빼먹으면 도구 상태가 stuck 될 수 있습니다. 드래그가 끝났는데 내부 상태는 아직 dragging이라고 믿는 상황입니다. 이런 버그는 재현이 애매해서 더 얄밉습니다.
데모에서 볼 것
데모에서는 드래그 도중 포인터를 영역 밖으로 빼도 이벤트가 유지되는지 확인합니다.
오늘의 핵심은 입력을 이벤트 나열이 아니라 상태 기계로 보는 것입니다. 선택 도구, pan 도구, resize 도구는 모두 같은 lifecycle 위에서 다른 계산만 얹습니다.