rotation handle
rotation handle은 선택 bounds 위쪽에 붙는 작은 손잡이입니다. 사용자가 이 손잡이를 드래그하면 선택된 도형이 중심을 기준으로 회전합니다.
중심에서 pointer까지의 각도를 구한다
회전은 각도 차이입니다. 먼저 pivot에서 pointer까지의 벡터를 만들고 atan2로 각도를 구합니다.
function angleFromPivot(pivot, pointer) {
return Math.atan2(pointer.y - pivot.y, pointer.x - pivot.x);
}
drag 시작 각도와 현재 각도의 차이가 rotation delta입니다.
deltaAngle = currentAngle - startAngle
pivot을 고정한다
회전할 때 중요한 것은 어떤 점을 고정할지입니다. 보통 selection center를 pivot으로 사용합니다.
pivot world point는 회전 전후 같은 위치에 있어야 한다.
center 기준 회전은 보기에는 단순하지만, node의 transform matrix를 갱신할 때 pivot 보정이 필요할 수 있습니다.
function beginRotate(selectionBounds, input, selectedNodes) {
const pivot = {
x: selectionBounds.x + selectionBounds.width / 2,
y: selectionBounds.y + selectionBounds.height / 2
};
return {
type: "rotate",
pivot,
startAngle: angleFromPivot(pivot, input.world),
initial: selectedNodes.map((node) => ({
id: node.id,
matrix: node.localMatrix
}))
};
}
function updateRotate(document, drag, currentWorld, options = {}) {
const rawDelta = angleFromPivot(drag.pivot, currentWorld) - drag.startAngle;
const delta = options.snap ? snapAngle(rawDelta, Math.PI / 12) : rawDelta;
return drag.initial.reduce((nextDocument, item) => {
const nextMatrix = rotateAroundPoint(item.matrix, drag.pivot, delta);
return updateNode(nextDocument, item.id, { localMatrix: nextMatrix });
}, document);
}
snapping도 각도에 적용된다
Shift를 누르면 15도 단위로 snap하는 식의 정책을 둘 수 있습니다.
function snapAngle(angle, stepRadians) {
return Math.round(angle / stepRadians) * stepRadians;
}
거리 snapping과 마찬가지로 raw 계산과 UX 정책을 분리하면 구현이 안정적입니다.
오늘의 핵심
rotation handle은 pivot 기준 각도 차이를 transform에 적용하는 도구입니다.
pivot
start pointer angle
current pointer angle
delta angle