viewport와 camera
Figma 같은 에디터에서 도형은 문서 위에 놓입니다. 사용자는 그 문서를 화면으로 바라봅니다. 이때 화면이 문서 전체를 그대로 보여주는 것은 아닙니다. 일부 영역을 확대하거나, 옆으로 밀거나, 축소해서 봅니다.
이 역할을 camera라고 부르겠습니다.
world와 screen 사이에 camera가 있다
도형의 위치는 world 좌표에 저장합니다.
const node = { x: 1200, y: 800, width: 240, height: 120 };
하지만 화면에 그릴 때는 camera를 적용해야 합니다.
screenX = (worldX - camera.x) * camera.zoom
screenY = (worldY - camera.y) * camera.zoom
camera의 x, y는 화면 왼쪽 위가 world 어디를 보고 있는지 나타냅니다. zoom은 world unit을 screen pixel로 얼마나 키울지 정합니다.
camera를 matrix로 표현한다
처음에는 공식으로 써도 되지만, 곧 node transform, group transform, WebGL uniform과 합쳐야 합니다. 그래서 camera도 3x3 matrix로 표현할 수 있어야 합니다.
function cameraToScreenMatrix(camera) {
const z = camera.zoom;
return [
z, 0, 0,
0, z, 0,
-camera.x * z, -camera.y * z, 1
];
}
function transformPoint(m, p) {
return {
x: m[0] * p.x + m[3] * p.y + m[6],
y: m[1] * p.x + m[4] * p.y + m[7]
};
}
const screenPoint = transformPoint(cameraToScreenMatrix(camera), worldPoint);
이 matrix는 screen = cameraMatrix * world라는 뜻입니다. CSS transform에서 배운 translate, scale 감각을 editor camera로 옮긴 형태입니다.
screen에서 world로 돌아가는 inverse matrix
input system은 반대 방향이 필요합니다. pointer event는 screen 좌표로 들어오고, document edit은 world 좌표에서 일어나기 때문입니다.
function screenToWorldMatrix(camera) {
const z = camera.zoom;
return [
1 / z, 0, 0,
0, 1 / z, 0,
camera.x, camera.y, 1
];
}
const worldPoint = transformPoint(screenToWorldMatrix(camera), screenPoint);
cameraToScreenMatrix와 screenToWorldMatrix는 서로 inverse 관계입니다. 둘이 어긋나면 보이는 위치와 클릭되는 위치가 달라집니다.
pan은 camera 위치를 바꾼다
사용자가 손 도구로 캔버스를 오른쪽으로 끌면 도형들이 오른쪽으로 움직이는 것처럼 보입니다. 하지만 실제로는 도형의 world 좌표를 바꾸면 안 됩니다.
바뀌는 것은 camera입니다.
node.x stays 1200
camera.x changes
이 구분이 중요합니다. pan할 때 모든 도형의 좌표를 바꾸면 undo/redo와 저장 모델이 이상해집니다. 화면을 움직인 것과 문서를 편집한 것은 다른 일입니다.
function panCamera(camera, screenDelta) {
return {
...camera,
x: camera.x - screenDelta.x / camera.zoom,
y: camera.y - screenDelta.y / camera.zoom
};
}
사용자가 화면을 오른쪽으로 40px 끌면, camera가 보는 world 위치는 왼쪽으로 40 / zoom만큼 이동합니다.
zoom은 camera와 함께 생각한다
zoom은 단순히 renderer scale만 바꾸는 일이 아닙니다. 포인터 좌표 변환, grid 간격, selection overlay, ruler label이 모두 zoom의 영향을 받습니다.
그래서 camera는 editor state의 독립된 일부로 둡니다.
const camera = {
x: 0,
y: 0,
zoom: 1
};
renderer만 이 값을 쓰는 것이 아닙니다. input system도 screen 좌표를 world 좌표로 바꾸기 위해 camera를 사용합니다.
function createCameraState() {
return {
x: 0,
y: 0,
zoom: 1,
minZoom: 0.05,
maxZoom: 32
};
}
오늘의 핵심
camera는 무한 캔버스에서 화면과 문서를 분리하는 장치입니다.
world: 문서 좌표
camera: 어느 부분을 어느 배율로 볼지
screen: 실제 화면 좌표
이 분리가 있어야 pan/zoom이 문서 편집과 섞이지 않습니다.