viewport와 camera 모델
이제 드디어 infinite canvas입니다.
무한 캔버스라고 해서 실제 DOM이 무한히 넓어지는 건 아닙니다. 브라우저 화면은 여전히 유한합니다. 대신 우리는 카메라를 움직입니다. 사용자는 문서 세계를 이동한다고 느끼지만, 구현 입장에서는 camera position과 zoom을 바꾸는 일입니다.
말하자면 “세상이 움직인 것처럼 보이게 카메라를 움직이는” 방식입니다.
viewport는 camera다
world 좌표의 점을 화면에 표시하려면 camera 위치를 빼고 zoom을 곱합니다.
screenX = (worldX - cameraX) * zoom
screenY = (worldY - cameraY) * zoom
반대로 화면 좌표에서 world 좌표를 알고 싶으면 zoom으로 나누고 camera를 더합니다.
worldX = screenX / zoom + cameraX
worldY = screenY / zoom + cameraY
이 공식이 viewport의 심장입니다. pan, zoom, ruler, grid, hit testing이 전부 여기에 기대고 있습니다. 꽤 중요한 친구죠.
CSS에서는 content layer를 움직인다
DOM 기반 infinite canvas에서는 보통 실제 오브젝트들을 담는 content layer를 하나 만들고, 여기에 viewport transform을 적용합니다.
<div class="stage-canvas" data-role="viewport">
<div class="world-layer" data-role="content">
<div class="shape" data-world-x="80" data-world-y="-20"></div>
</div>
</div>
.stage-canvas {
position: relative;
overflow: hidden;
}
.world-layer {
position: absolute;
inset: 0;
transform-origin: 0 0;
transform: translate(centerX, centerY)
scale(zoom)
translate(-cameraX, -cameraY);
}
.shape {
position: absolute;
left: var(--world-x);
top: var(--world-y);
}
transform-origin: 0 0으로 고정하면 수학 공식과 CSS 결과를 맞추기 쉽습니다. 기본 center origin을 그대로 두면 pan/zoom 계산이 필요 이상으로 요란해집니다.
pan은 camera를 바꾸는 일이고, zoom은 scale을 바꾸는 일입니다. 이때 child object의 world 좌표는 유지합니다. 카메라가 움직였다고 문서 속 오브젝트의 위치가 바뀌면 안 됩니다.
여기서 살짝 장난스러운 포인트가 있습니다. 오브젝트는 가만히 있는데 content layer만 움직입니다. 그래서 DOM을 보면 child의 left/top은 그대로고, 부모인 .world-layer의 transform만 계속 바뀝니다. 화면에서는 세상이 움직이는 것처럼 보이지만, 사실은 카메라 담당 div가 열심히 일하는 중입니다.
데모에서 볼 것
데모에서는 오른쪽 아래 DOM camera stack 패널을 보세요. stage-canvas는 viewport 역할을 하고, world-layer는 content 역할을 합니다. 슬라이더를 움직이면 child shape의 world 좌표는 그대로인데 .world-layer의 matrix 값만 바뀝니다.
마우스를 캔버스 위에서 움직이면 cursor local, centered screen, world 좌표가 같이 갱신됩니다. 기본 wheel은 cursor 기준 zoom이고, Space를 누른 상태의 wheel/trackpad scroll은 pan입니다. 이 세 좌표와 입력 모드가 구분되기 시작하면 viewport는 더 이상 마법 상자가 아닙니다. 그냥 좌표 변환 기계입니다. 말은 좀 무뚝뚝한데, 그래픽 에디터 입장에서는 꽤 든든한 친구입니다.
오늘의 핵심은 infinite canvas를 “큰 DOM”으로 보지 않는 것입니다. 문서는 world에 있고, 사용자는 camera를 통해 봅니다. 이 관점이 잡히면 viewport가 갑자기 단순해집니다.