ruler와 tick 계산
ruler는 화면 위에 얹힌 작은 눈금자처럼 보입니다. 그런데 사실은 world 좌표를 screen 위에 번역해 보여주는 장치입니다.
여기서 재미있는 점은 위치와 의미가 서로 다른 좌표계를 쓴다는 겁니다. tick의 위치는 screen pixel이고, label의 값은 world 좌표입니다. 겉보기에는 조용하지만 속은 꽤 바쁩니다.
tick은 world에서 만들고 screen에 그린다
현재 보이는 world range를 먼저 구합니다. 그리고 그 범위 안에서 일정 간격으로 tick을 만듭니다.
이 수식은 행렬을 안 쓰는 대충 버전이 아닙니다. 2D camera 행렬을 ruler의 x축 한 줄에만 풀어 쓴 형태입니다.
screen = Scale(zoom) * Translate(-camera) * world
이걸 x축 tick 하나에만 적용하면 이렇게 됩니다.
tickScreen = (tickWorld - camera) * zoom
여기서 중요한 질문은 “camera가 무엇을 뜻하냐”입니다. 에디터 본문에서는 보통 camera를 viewport 중심으로 둡니다. 그런데 ruler overlay에서는 화면 왼쪽 끝을 기준으로 잡으면 계산이 더 단순할 때가 많습니다.
cameraLeft = viewport 왼쪽 끝에 해당하는 world x
tickScreen = (tickWorld - cameraLeft) * zoom
예를 들어 cameraLeft = -75, zoom = 1.4, tickWorld = 0이면:
tickScreen = (0 - (-75)) * 1.4
= 105px
이 말은 world의 x = 0 눈금이 ruler overlay의 왼쪽에서 105px 떨어진 곳에 찍힌다는 뜻입니다.
같은 장면을 viewport 중심 camera 모델로 쓰면 기준 offset이 하나 더 붙습니다.
cameraCenter = cameraLeft + viewportWidth / (2 * zoom)
domX = viewportCenterX + (tickWorld - cameraCenter) * zoom
둘은 서로 다른 수식이 아니라 기준점을 다르게 잡은 같은 변환입니다. 왼쪽 기준 ruler는 cameraLeft를 쓰고, 중앙 기준 infinite canvas는 cameraCenter와 viewportCenterX를 같이 씁니다. 이걸 구분하면 머리가 덜 어지럽습니다. 그래픽 수학에서 머리 덜 어지러운 건 꽤 큰 복지입니다.
이때 label은 tickWorld 값을 보여주고, 실제 CSS 위치는 tickScreen 또는 domX를 씁니다.
보기 좋은 간격 고르기
zoom이 바뀌면 같은 world step도 화면에서 너무 좁거나 넓어질 수 있습니다. 그래서 목표 screen 간격을 정한 뒤, 그에 맞는 world step을 고릅니다.
targetWorldStep = targetScreenPx / zoom
niceStep = 1, 2, 5 * 10^n 중 가까운 값
1, 2, 5 계열은 ruler에서 자주 씁니다. 사람 눈에 익숙하고 label이 지저분해지지 않습니다. 37.418 같은 눈금이 잔뜩 나오면 교육용 데모가 아니라 계산기 벌칙처럼 보입니다.
overlay에 둔다
ruler는 content transform에 직접 포함하지 않는 편이 좋습니다. 화면 가장자리에 고정된 overlay로 두고, 내부 tick만 camera/zoom에 맞춰 계산합니다.
major/minor tick을 나누면 zoom 변화 중에도 더 안정적으로 보입니다. guide, snap distance label도 같은 tick 생성 정책을 공유하면 전체 도구의 감각이 통일됩니다.
데모에서 볼 것
데모에서는 zoom을 바꾸며 ruler step이 1/2/5 계열로 바뀌는 순간을 확인합니다.
오늘의 핵심은 ruler가 단순한 장식이 아니라 좌표 변환 UI라는 점입니다. 위치는 screen, 의미는 world. 이 둘을 같이 들고 가야 합니다.