점, 벡터, 거리와 기본 용어
지난 시간에는 좌표계에 이름표를 붙였습니다. 이제 그 좌표 위에서 움직여봅시다.
그래픽 에디터에서 사용자가 뭔가를 드래그하면, 사실 브라우저는 이렇게 말하는 중입니다.
“아까는 여기였고, 지금은 여기네요.”
이 두 점의 차이가 바로 벡터입니다. 멋진 이름을 붙이면 살짝 어려워 보이지만, 벡터는 그냥 “얼마나, 어느 방향으로 움직였는가”입니다. 편집기 입장에서는 아주 성실한 일꾼입니다. 이동, 거리 측정, 스냅, 가이드, 핸들 드래그가 전부 이 친구를 부릅니다.
점과 벡터를 구분하자
점은 위치입니다.
p = (x, y)
벡터는 변화량입니다.
v = (dx, dy)
둘 다 숫자 두 개라서 생긴 건 비슷합니다. 그래서 초반에 자주 헷갈립니다. 그런데 역할이 다릅니다. 점은 “어디?”에 답하고, 벡터는 “어느 쪽으로 얼마나?”에 답합니다.
드래그를 생각해보면 바로 감이 옵니다.
delta = current - start
시작점이 (100, 80)이고 현재점이 (140, 110)이면 delta는 (40, 30)입니다. 오브젝트를 오른쪽으로 40px, 아래로 30px 옮기면 되겠네요. 여기서 편집기는 갑자기 똑똑해진 것이 아닙니다. 그냥 뺄셈을 잘한 겁니다.
길이는 피타고라스다
벡터의 길이는 거리입니다.
length = sqrt(dx * dx + dy * dy)
“마우스가 핸들에 충분히 가까운가?”, “가이드 라인에 붙일 만큼 가까운가?”, “두 점 사이에 선을 그리면 얼마나 긴가?” 같은 질문은 대부분 이 공식으로 시작합니다.
다만 편집기는 매 프레임 바쁩니다. 그래서 단순히 가까운지 비교만 할 때는 sqrt를 생략하고 제곱 거리끼리 비교하기도 합니다.
distanceSquared = dx * dx + dy * dy
예를 들어 반경 8px 안에 들어왔는지만 알고 싶다면, 실제 거리와 8을 비교하지 않고 제곱 거리와 64를 비교해도 됩니다. 계산을 덜 해도 같은 결론이 나옵니다. 수학이 가끔 이렇게 알뜰합니다.
방향만 필요할 때는 단위 벡터
길이를 1로 만든 벡터를 단위 벡터라고 부릅니다.
unit = delta / length
방향만 알고 싶을 때 유용합니다. 예를 들어 선택 박스의 핸들을 잡고 어느 방향으로 늘릴지, 회전 핸들이 중심에서 어느 방향에 있는지, 선분 위에 점을 투영할 때 어느 축을 기준으로 삼을지 같은 곳에서 계속 등장합니다.
그리고 두 벡터가 얼마나 같은 방향을 보고 있는지는 dot product로 확인합니다.
dot(a, b) = ax * bx + ay * by
이 공식은 뒤에서 projection, gradient, snapping을 만날 때 다시 나옵니다. 도망가도 소용없습니다. 그래도 괜찮습니다. 자주 보면 정듭니다.
편집기 수학 용어 미리보기
이제 뒤 강의에서 계속 나올 용어를 미리 정리해둡시다. 처음부터 전부 외울 필요는 없습니다. “아, 저 단어는 이런 느낌이었지” 정도만 잡아도 충분합니다. 용어는 길어 보여도, 대부분은 사각형과 점을 다루는 말입니다.
bounds는 어떤 도형이 차지하는 범위입니다. 보통 left, top, right, bottom 네 값으로 표현합니다.
bounds = { left, top, right, bottom }
width = right - left
height = bottom - top
corner는 bounds의 네 꼭짓점입니다.
topLeft, topRight, bottomRight, bottomLeft
회전이나 scale을 적용하기 전에는 사각형의 corner를 구하기 쉽습니다. 그런데 transform이 들어가면 각 corner를 matrix로 변환해야 합니다.
transformedCorner = M * corner
AABB는 Axis-Aligned Bounding Box의 줄임말입니다. 축에 맞춰진 감싸는 사각형이라는 뜻입니다. 말이 길죠. 쉽게 말하면 “화면 x축, y축에 반듯하게 맞춘 선택 박스”입니다.
회전된 사각형 자체는 기울어져 있지만, 그걸 화면 축에 맞는 사각형으로 감싸면 AABB가 됩니다.
minX = min(corner.x)
maxX = max(corner.x)
minY = min(corner.y)
maxY = max(corner.y)
AABB = { left: minX, top: minY, right: maxX, bottom: maxY }
그래서 회전된 오브젝트의 AABB는 원래 width/height보다 커질 수 있습니다. 브라우저의 getBoundingClientRect()도 이런 식의 화면 축 기준 bounds로 생각하면 이해가 쉽습니다.
OBB는 Oriented Bounding Box입니다. 오브젝트의 회전을 따라 같이 기울어진 박스입니다. 정확한 선택 판정에는 OBB나 local bounds가 좋고, 빠른 후보 검사에는 AABB가 좋습니다.
AABB: 화면 축에 맞춘 빠른 박스
OBB: 오브젝트 방향을 따라가는 정확한 박스
projection은 점이나 벡터를 어떤 축 위에 떨어뜨려 보는 계산입니다. dot product로 구합니다.
t = dot(point - origin, axis)
이 값은 gradient, snapping, resize handle에서 계속 나옵니다. “이 점이 이 축을 따라 얼마나 가 있지?”라는 질문입니다.
union은 여러 bounds를 하나로 합치는 일입니다. 여러 오브젝트를 선택했을 때 selection bounds를 만들 때 씁니다.
union.left = min(a.left, b.left)
union.right = max(a.right, b.right)
intersection은 두 bounds가 겹치는지 보는 일입니다. 마키 선택에서 selection rectangle과 오브젝트 bounds가 만나는지 확인할 때 씁니다.
이 용어들은 뒤에서 계속 다시 만납니다. 겁먹을 필요는 없습니다. 교수님이 새 단어인 척 다시 꺼내도, 사실 오늘 본 친구들입니다.
DOM에서는 이렇게 쓴다
pointerdown에서 시작점을 저장하고, pointermove마다 현재점과의 차이를 계산합니다.
start = pointerdown position
current = pointermove position
delta = current - start
프리뷰 이동은 보통 left/top을 계속 바꾸기보다 transform: translate(dx, dy)를 씁니다. 사용자가 드래그하는 동안에는 빠르게 보여주고, pointerup에서 모델의 실제 좌표에 반영하는 방식이 편합니다.
transform: translate(40px, 30px);
이렇게 하면 화면 확대/축소, selection overlay, undo 기록을 분리해서 다루기 쉬워집니다.
데모에서 볼 것
데모에서는 두 점을 움직이며 delta, length, unit vector가 어떻게 바뀌는지 봅니다.
여기서 중요한 건 숫자 자체가 아닙니다. “점 두 개의 차이가 편집 동작 하나가 된다”는 감각입니다. 이 감각이 생기면 드래그는 더 이상 이벤트 처리 문제가 아니라 좌표 계산 문제가 됩니다.
편집기의 첫 번째 마법은 대단한 알고리즘이 아니라, 시작점과 현재점을 빼는 일입니다. 김빠지죠? 그런데 이 김빠지는 계산이 나중에 꽤 멋진 도구가 됩니다.