resize와 rotation handle
이번에는 핸들을 실제로 드래그했을 때 크기와 회전을 어떻게 계산하는지 봅니다.
사용자는 corner handle을 잡고 끌 뿐입니다. 하지만 에디터 내부에서는 anchor, pointer, local axis, scale, aspect ratio가 같이 움직입니다. 겉보기보다 꽤 바쁜 순간입니다.
핸들은 AABB인가 OBB인가
단일 회전 오브젝트를 편집할 때 resize handle은 보통 OBB 기준으로 붙입니다.
OBB = object local bounds transformed by object world matrix
즉, 오브젝트가 24도 회전해 있으면 selection outline과 corner handle도 24도 같이 돌아갑니다. 사용자는 기울어진 사각형의 모서리를 잡고 있다고 느끼기 때문입니다.
AABB는 다른 용도에 더 가깝습니다.
AABB
screen/world 축에 평행한 빠른 bounds
broad phase hit testing
marquee selection 후보
여러 오브젝트 selection union
OBB
오브젝트의 회전된 local bounds
단일 오브젝트 resize handle
rotation handle 기준 frame
그래서 데모에서는 초록색 outline과 handle은 OBB 기준으로 그리고, 흐린 dashed box는 AABB 참고용으로만 보여줍니다. AABB에 핸들을 붙이면 회전된 오브젝트를 늘릴 때 사용자가 잡은 방향과 실제 scale 축이 어긋납니다. 이러면 에디터가 살짝 장난치는 것처럼 느껴집니다. 장난은 교수님만 치고, UI는 정직해야 합니다.
핸들 사각형 자체도 선택 프레임의 회전 방향에 맞춰 돌립니다. 핸들 위치만 OBB corner에 붙이고 사각형은 화면 축에 고정해도 동작은 하지만, resize 축을 보여주는 교육용 데모에서는 같이 돌리는 편이 훨씬 읽기 좋습니다.
transform-origin이 수식을 바꾸나?
결론부터 말하면, 바꿉니다. 조금 더 정확히는 브라우저가 transform 앞뒤에 origin 보정 이동을 끼워 넣습니다.
effectiveTransform =
Translate(origin)
* Transform
* Translate(-origin)
transform-origin: 0 0이면 local top-left를 기준으로 회전합니다.
world = Translate(position) * Rotate(angle) * localPoint
반면 transform-origin: center center이면 중심을 기준으로 회전합니다. top-left 기준 local 좌표를 그대로 쓰는 모델에서는 이렇게 생각할 수 있습니다.
world =
Translate(topLeft)
* Translate(width/2, height/2)
* Rotate(angle)
* Translate(-width/2, -height/2)
* localPoint
에디터 모델에서는 아예 local 좌표계를 중심 기준으로 잡으면 더 단순해집니다.
local bounds = [-w/2, -h/2] ... [w/2, h/2]
world = Translate(center) * Rotate(angle) * localPoint
이번 데모는 이 center-origin 모델을 쓰되, CSS의 숨은 transform-origin: center center 보정에 기대지 않습니다. DOM box 자체는 여전히 top-left에서 시작하기 때문에, 그 차이를 matrix에 명시합니다.
drawMatrix =
Translate(center)
* Rotate(angle)
* Translate(-width/2, -height/2)
그리고 CSS는 이렇게 둡니다.
transform-origin: 0 0;
transform: matrix(...drawMatrix);
즉 수학 모델의 origin은 center이고, 실제 DOM box를 그릴 때만 Translate(-width/2, -height/2)로 top-left box를 center local 좌표계에 맞춥니다. 이렇게 하면 OBB overlay, handle 위치, 실제 그려진 div가 같은 matrix 이야기를 합니다. CSS origin과 수학 모델의 origin이 다르면 overlay와 실제 DOM이 슬쩍 어긋납니다. 슬쩍 어긋나는 UI가 제일 얄밉습니다.
resize는 반대편 anchor를 고정한다
corner handle을 잡고 늘릴 때 보통 반대편 corner는 고정점이 됩니다. 드래그 중인 handle 위치와 고정점 사이의 거리가 새 크기를 만듭니다.
scaleX = newWidth / oldWidth
scaleY = newHeight / oldHeight
회전된 도형을 resize할 때는 포인터를 selection local 좌표로 되돌린 뒤 계산하는 편이 쉽습니다. 화면 좌표에서 바로 계산하려고 하면 축이 기울어져 있어서 머리가 복잡해집니다.
데모에서는 corner handle을 드래그할 때 반대쪽 OBB corner를 anchor로 고정합니다. 현재 포인터와 anchor 사이의 벡터를 오브젝트의 local x/y 축에 projection해서 새 width/height를 구합니다.
delta = pointer - anchor
newWidth = dot(delta, localXAxis) * handleSignX
newHeight = dot(delta, localYAxis) * handleSignY
Shift를 누른 상태로 resize하면 aspect ratio를 유지합니다. handle이 anchor를 지나가면 width/height가 음수가 될 수 있는데, 이번 데모에서는 flip을 허용하지 않고 최소 크기에서 막습니다. flip 허용은 별도 정책으로 다루는 편이 좋습니다.
rotation handle은 각도 차이다
회전은 중심에서 시작 포인터와 현재 포인터를 바라보는 각도의 차이입니다.
rotationDelta = atan2(current - center) - atan2(start - center)
각도 wrap 처리는 여기서도 필요합니다. 359도에서 1도로 넘어갈 때 큰 회전으로 해석하면 오브젝트가 갑자기 과격해집니다.
modifier 정책
Shift는 aspect ratio lock, Alt는 center resize처럼 modifier를 둘 수 있습니다.
aspect lock: scaleY = scaleX or derived by ratio
최소 크기와 flip 허용 여부도 정해야 합니다. handle이 반대편 anchor를 지나가면 width가 음수가 될 수 있습니다. 이때 flip으로 볼지, 최소 크기에서 막을지 제품 정책을 정해야 합니다.
데모에서 볼 것
데모에서는 회전된 도형을 리사이즈하며 local 좌표로 계산해야 하는 이유를 확인합니다.
corner handle을 직접 잡아 resize해보고, 위쪽의 동그란 rotation handle을 잡아 회전해보세요. Shift를 누른 채 회전하면 15도 단위로 snap됩니다. dashed AABB는 움직임을 이해하기 위한 참고선이고, 실제 조작 handle은 OBB 위에 있습니다.
오늘의 핵심은 handle drag를 “마우스가 움직였다”로 보지 않고 “고정점과 현재점 사이의 관계가 바뀌었다”로 보는 것입니다. 그러면 resize와 rotate가 훨씬 차분해집니다.