screen/world/local 좌표계 복습
GPU 에디터에서도 좌표계 문제는 사라지지 않습니다. 오히려 DOM이 도와주던 부분이 사라져서 더 명시적으로 다뤄야 합니다.
이번 레슨에서는 세 좌표계를 다시 고정합니다.
screen: 화면 기준
world: 문서 기준
local: 도형 내부 기준
screen 좌표는 입력과 렌더링의 표면이다
pointer event는 screen에 가까운 좌표를 줍니다. 정확히는 viewport 기준 clientX, clientY에서 Canvas rect를 빼서 canvas local screen 좌표를 만듭니다.
const screen = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
selection handle, toolbar, inspector 같은 UI도 보통 screen 좌표에 가깝게 생각합니다.
function clientToCanvas(event, canvas) {
const rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
world 좌표는 문서의 안정적인 기준이다
도형 위치는 world에 저장합니다. camera가 움직여도 world 좌표는 바뀌지 않습니다.
const world = {
x: screen.x / camera.zoom + camera.x,
y: screen.y / camera.zoom + camera.y
};
이 변환은 hit testing, drawing, snapping에서 계속 반복됩니다.
function screenToWorld(screen, camera) {
return {
x: screen.x / camera.zoom + camera.x,
y: screen.y / camera.zoom + camera.y
};
}
function worldToScreen(world, camera) {
return {
x: (world.x - camera.x) * camera.zoom,
y: (world.y - camera.y) * camera.zoom
};
}
local 좌표는 도형 내부 기준이다
회전된 도형을 클릭했는지 정확히 알고 싶으면 world point를 node local 좌표로 바꿉니다.
local = inverse(nodeWorldMatrix) * world
그다음 local point가 0 <= x <= width, 0 <= y <= height 안에 있는지 검사합니다. 이 방식은 사각형, 이미지, 텍스트 박스 같은 도형에서 기본이 됩니다.
2D affine matrix 최소 구현
Part 1부터 아래 정도의 matrix helper는 익숙해져야 합니다. 뒤에서 group transform, hit test, WebGL uniform이 모두 이 형태로 이어집니다.
function identity3() {
return [1, 0, 0, 0, 1, 0, 0, 0, 1];
}
function translate3(tx, ty) {
return [1, 0, 0, 0, 1, 0, tx, ty, 1];
}
function scale3(sx, sy) {
return [sx, 0, 0, 0, sy, 0, 0, 0, 1];
}
function rotate3(rad) {
const c = Math.cos(rad);
const s = Math.sin(rad);
return [c, s, 0, -s, c, 0, 0, 0, 1];
}
곱셈은 “오른쪽 변환을 먼저 적용하고, 그 결과에 왼쪽 변환을 적용한다”고 읽으면 됩니다.
function multiply3(a, b) {
return [
a[0] * b[0] + a[3] * b[1] + a[6] * b[2],
a[1] * b[0] + a[4] * b[1] + a[7] * b[2],
a[2] * b[0] + a[5] * b[1] + a[8] * b[2],
a[0] * b[3] + a[3] * b[4] + a[6] * b[5],
a[1] * b[3] + a[4] * b[4] + a[7] * b[5],
a[2] * b[3] + a[5] * b[4] + a[8] * b[5],
a[0] * b[6] + a[3] * b[7] + a[6] * b[8],
a[1] * b[6] + a[4] * b[7] + a[7] * b[8],
a[2] * b[6] + a[5] * b[7] + a[8] * b[8]
];
}
local point를 world로 보낼 때는 node matrix를 적용하고, world point를 local로 되돌릴 때는 inverse matrix를 적용합니다.
function invert3(m) {
const a00 = m[0], a01 = m[1], a02 = m[2];
const a10 = m[3], a11 = m[4], a12 = m[5];
const a20 = m[6], a21 = m[7], a22 = m[8];
const b01 = a22 * a11 - a12 * a21;
const b11 = -a22 * a10 + a12 * a20;
const b21 = a21 * a10 - a11 * a20;
const det = a00 * b01 + a01 * b11 + a02 * b21;
if (Math.abs(det) < 1e-8) {
throw new Error("matrix is not invertible");
}
const invDet = 1 / det;
return [
b01 * invDet,
(-a22 * a01 + a02 * a21) * invDet,
(a12 * a01 - a02 * a11) * invDet,
b11 * invDet,
(a22 * a00 - a02 * a20) * invDet,
(-a12 * a00 + a02 * a10) * invDet,
b21 * invDet,
(-a21 * a00 + a01 * a20) * invDet,
(a11 * a00 - a01 * a10) * invDet
];
}
function pointInNode(worldPoint, node) {
const local = transformPoint(invert3(node.worldMatrix), worldPoint);
return local.x >= 0 && local.y >= 0 && local.x <= node.width && local.y <= node.height;
}
오늘의 핵심
좌표계는 이름표입니다. 이름표가 없으면 숫자는 금방 위험해집니다.
screen -> world: camera inverse
world -> local: node matrix inverse
local -> world: node matrix
world -> screen: camera
GPU 렌더러는 API가 달라져도 이 좌표계 구분을 그대로 사용합니다.