Published on

웹 프론트 Pan Zoom 개발기

서론

지금 다니는 회사에서는 위성영상을 인공지능으로 분석해 사용자에게 정보를 제공하는 제품을 만들고 있습니다. 그중 최근 위성영상을 세그멘테이션하는 툴을 만들게 되었습니다. 위성영상을 확대/축소하고 이동하는 기능이 어려울 것이라 생각했지만, 기존에 비슷한 툴을 한번 만들어 본 적이 있어 코드를 어느 정도 가져오면 되겠다고 생각했습니다. 하지만 마음처럼 되지 않았고, 공식을 정리하고 기록해 둘 필요를 느껴 이 글을 작성하게 되었습니다.

요구사항

먼저 요구사항 중에는 줌과 이동 기능이 있었습니다. 다만 줌을 할 때에는 마우스 포인터를 기준으로 잡고 줌 인 / 아웃을 해야 했습니다(아래 영상 참조). 그리고 이동은 여느 프로그램처럼 드래그로 이동하면 되었습니다.

네이버 지도의 줌 인 / 아웃

ZOOM

먼저 어려울 것 같은 줌부터 해결해 보았습니다. 이걸 어떻게 구현해야 하나 고민하고 있을 때, 마침 회사 동료분이 졸업작품으로 비슷한 것을 구현하신 적이 있었습니다. 그분께 조언을 구하니 동차 변환 행렬이라는 것을 찾아보라고 알려주셨습니다. 찾아보니 잘 정리된 블로그가 있었습니다. 행렬을 대수식으로 표현하면 다음과 같은 식이 나옵니다.

x′=sxx+px(1−sx)x' = s_x x + p_x(1 - s_x) y′=syy+py(1−sy)y' = s_y y + p_y(1 - s_y)

여기에서 ss는 확대 비율을 의미합니다. xx는 기존의 좌표를, pp는 기준이 되는 좌표를 의미합니다.

예를 들어 (1,1)(1, 1)을 (2,2)(2, 2)를 기준으로 2배 확대하였을 때의 좌표는 다음 계산 결과에 따라 (0,0)(0, 0)이 됩니다.

x:2×1+2(1−2)=0x : 2 \times 1 + 2(1 - 2) = 0 y:2×1+2(1−2)=0y : 2 \times 1 + 2(1 - 2) = 0

이것을 코드로 적용해 보았습니다.

const leftTop = new Point(0, 0)

function zoom(zoomLevel: number) {
  const newX = zoomLevel * leftTop.x + relativePoint.x * (1 - zoomLevel)
  const newY = zoomLevel * leftTop.y + relativePoint.y * (1 - zoomLevel)
  return new Point(newX, newY)
}

PAN

마우스를 잡고 드래그하여 위치를 옮기는 것을 Panning이라고 부릅니다. 패닝된 좌표를 구하는 방법은 굉장히 쉽습니다. 다들 눈치채셨겠지만, 마우스의 무브먼트만큼 기존 좌표에 더해주면 됩니다.

const leftTop = new Point(0, 0)

const panningEventHandler = (e: MouseEvent) => {
  return new Point(leftTop.x + e.movementX, leftTop.y + e.movementY)
}

결과물

저는 위 코드와 SVG의 transform matrix를 이용하여 Pan, Zoom 요구사항을 구현하였습니다.

const leftTop = new Point(0, 0)

function zoom(zoomLevel: number) {
  const newX = zoomLevel * leftTop.x + relativePoint.x * (1 - zoomLevel)
  const newY = zoomLevel * leftTop.y + relativePoint.y * (1 - zoomLevel)
  return new Point(newX, newY)
}

const panningEventHandler = (e: MouseEvent) => {
  return new Point(leftTop.x + e.movementX, leftTop.y + e.movementY)
}

// matrix의 0, 3번째 자리에 줌값을, 4번째 자리엔 x값을, 5번째 자리엔 y값을 넣어주면 됩니다.
<svg transform="matrix(1, 0, 0, 1, 0, 0)"></svg>