Published on

웹 프론트 Pan Zoom 개발기

Authors
  • avatar
    Name
    Mingyu Kim
    Twitter

서론

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

요구사항

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

네이버 지도의 줌 인 / 아웃

ZOOM

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

x=sxx+px(1sx)x' = s_x x + p_x(1 - s_x) y=syy+py(1sy)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(12)=0x : 2 \times 1 + 2(1 - 2) = 0 y:2×1+2(12)=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>