CSS Graphics Geometry
Guide
Reference
GitHub
Guide
Reference
GitHub
  • Part 0. Math foundations

    • 좌표계와 CSS pixel
    • 점, 벡터, 거리와 기본 용어
    • 각도, 라디안, atan2
  • Part 1. Transform linear algebra

    • translate, scale, rotate
    • CSS matrix(a,b,c,d,e,f)
    • 변환 순서와 transform-origin
    • local, world, screen 좌표계
    • inverse matrix와 포인터 입력
    • 좌표와 transform 디버깅
  • Part 2. Infinite canvas math and architecture

    • viewport와 camera 모델
    • cursor anchored zoom
    • ruler와 tick 계산
    • 주기 함수와 grid step
    • content, overlay, controls layer
    • scene graph와 nested transform
  • Part 3. Editor tool math

    • Pointer Events와 드래그
    • DOMRect와 getBoundingClientRect
    • hit testing과 bounding box
    • selection rectangle과 marquee
    • selection bounds와 handles
    • resize와 rotation handle
    • snapping과 smart guides
    • group, frame, clipping
  • Part 4. CSS graphics properties as math

    • CSS box로 도형 만들기
    • clip-path, radius, shadow
    • 선형보간과 linear-gradient
    • 거리 함수와 radial/conic-gradient
    • stacking context와 합성
    • layout, paint, composite
  • Part 5. Editor state and persistence

    • 레이어 모델과 z-index
    • 이동, 복제, 삭제, 잠금
    • undo/redo command model
    • JSON export/import
  • Part 6. SVG overlay and vector editing

    • SVG viewBox와 좌표계
    • getScreenCTM과 inverse
    • SVG pointer-events와 stroke hit testing
    • cubic bezier path editing
    • mask, clipPath, marker
  • Part 7. Figma to CSS translation

    • Figma node와 CSS DOM 모델
    • Frame constraints와 Auto Layout
    • Fills, strokes, effects를 CSS로
    • Vector와 text 변환
    • gradient 밖의 CSS 수학
    • Figma gradient를 CSS gradient로 변환하기
    • 수학이 필요한 Figma to CSS 속성들
    • Figma VectorNetwork 정리
  • Appendix A. Canvas renderer transition

    • Canvas로 넘어가는 기준
  • Appendix B. Motion and timing

    • 모션 수학과 timing 함수
    • Keyframe timeline 엔진
    • Motion path 수학
    • Bezier curve 길이 구하기

좌표계와 CSS pixel

그래픽 툴을 만든다고 하면 보통 이런 걸 먼저 떠올립니다. 사각형 도구, 펜툴, 레이어 패널, 색상 피커. 맞습니다. 다 중요합니다.

그런데 막상 만들기 시작하면 첫 번째 질문은 아주 소박합니다.

“그래서 마우스가 지금 어디 있는데요?”

농담 같지만 진짜입니다. 편집기의 거의 모든 일은 이 질문에서 출발합니다. 사용자가 클릭한 곳이 빈 캔버스인지, 도형 위인지, 리사이즈 핸들 위인지 알아야 하니까요. 브라우저는 이 질문에 감성적으로 답하지 않습니다. 아주 차갑게 숫자 두 개를 줍니다.

p = (x, y)

이 두 숫자가 우리가 앞으로 계속 만질 좌표입니다. CSS로 박스를 하나 그릴 때도, 드래그로 도형을 옮길 때도, Figma 같은 에디터에서 selection box를 그릴 때도 결국은 “어떤 점이 어디에 있는가”를 계산합니다.

먼저 감 잡기

  • CSS에서 위치와 크기를 다룰 때 기본 단위는 CSS pixel입니다.
  • 마우스 좌표는 숫자만 보면 안 되고, “어디를 기준으로 잰 숫자인가”를 같이 봐야 합니다.
  • 편집기 입력은 대부분 한 좌표계의 점을 다른 좌표계의 점으로 바꾸는 일입니다.

이번 강의에서 제일 중요한 문장은 이겁니다.

“좌표는 혼자 존재하지 않습니다. 항상 기준점과 같이 다닙니다.”

이걸 놓치면 뒤에서 이상한 일이 벌어집니다. 분명 마우스는 도형 위에 있는데 hit test가 안 되고, selection box는 한 20px쯤 옆에서 따라오고, ruler 숫자는 그럴듯한데 실제 도형 위치와 안 맞습니다. 이런 버그는 대부분 수학이 어려워서가 아니라, 좌표계 이름표를 안 붙여서 생깁니다.

CSS pixel은 진짜 pixel이 아니다

CSS에서 width: 100px이라고 쓰면, 처음에는 화면의 물리적인 점 100개를 차지한다고 생각하기 쉽습니다. 이름이 pixel이니까요. 이름이 사람을 속입니다.

브라우저에서 말하는 px는 물리 pixel이 아니라 CSS pixel입니다. CSS layout을 계산하기 위한 논리 단위라고 보는 게 좋습니다.

예를 들어 고해상도 디스플레이에서 devicePixelRatio = 2라면, CSS로 100px짜리 박스를 만들었을 때 브라우저는 내부적으로 대략 200 x 200 장치 pixel을 써서 더 선명하게 그릴 수 있습니다.

model width = 100 CSS px
screen width = 100 CSS px
device pixels = 100 * devicePixelRatio

DOM 기반 그래픽 툴에서는 일단 CSS pixel을 기준으로 생각하면 됩니다. 도형의 x, y, width, height를 CSS pixel 단위로 저장하고, DOM style로 옮겨 그리는 식입니다.

Canvas로 넘어가면 약간 더 귀찮아집니다. Canvas는 backing store 크기를 직접 맞춰줘야 해서 devicePixelRatio를 더 신경 써야 합니다. 지금은 “아, CSS pixel과 물리 pixel은 다른 녀석이구나” 정도만 잡고 가면 됩니다. 나중에 Canvas 강의에서 다시 혼내주겠습니다.

브라우저 좌표계는 왼쪽 위가 원점이다

수학 시간에 익숙한 좌표계는 보통 이렇게 생겼습니다. 오른쪽이 +x, 위쪽이 +y. 아주 반듯하죠.

브라우저는 살짝 다릅니다.

(0, 0) ----------------> +x
  |
  |
  v
 +y

왼쪽 위가 원점이고, 오른쪽으로 가면 x가 커지고, 아래로 내려가면 y가 커집니다.

처음 보면 “왜 y가 아래로 커지죠?” 싶습니다. 그런데 브라우저 입장에서는 꽤 자연스럽습니다. 문서는 위에서 아래로 흐르고, 화면도 왼쪽 위부터 채워진다고 생각할 수 있으니까요.

여기서 작은 함정이 하나 있습니다. 위로 드래그하면 y가 줄어듭니다.

아래로 이동: y 증가
위로 이동: y 감소

이걸 대충 넘기면 나중에 rotation handle이나 각도 계산에서 고개를 갸웃하게 됩니다. “어? 시계 방향이 왜 양수 같지?” 같은 순간이 옵니다. 그때 오늘 배운 이 좌표계가 뒤에서 조용히 웃고 있는 겁니다.

client 좌표: 브라우저 창 기준

포인터 이벤트에서 가장 자주 만나는 값은 clientX, clientY입니다.

element.addEventListener("pointermove", (event) => {
  console.log(event.clientX, event.clientY);
});

여기서 client는 viewport 기준입니다. viewport는 지금 브라우저 창에서 실제로 보이는 영역입니다.

예를 들어 clientY = 20이면 이런 뜻입니다.

“현재 보이는 브라우저 화면의 위쪽에서 20px 아래에 있다.”

페이지가 스크롤되어 있든 말든 client는 지금 보이는 창을 기준으로 합니다. 그래서 pointer event를 다룰 때 출발점으로 쓰기 좋습니다.

하지만 그래픽 에디터에서는 이 값만으로 부족합니다. 우리는 보통 이런 걸 알고 싶습니다.

“브라우저 창에서 어디냐”가 아니라,

“캔버스 안에서 어디냐.”

사용자가 캔버스 왼쪽 위에서 얼마나 떨어진 지점을 누르고 있는지 알아야 사각형을 만들고, 선택하고, 드래그할 수 있습니다.

local 좌표: 특정 요소 안에서의 좌표

여기서 등장하는 친구가 getBoundingClientRect()입니다.

const rect = canvas.getBoundingClientRect();

const local = {
  x: event.clientX - rect.left,
  y: event.clientY - rect.top
};

이 코드는 굉장히 단순해 보입니다. 그런데 편집기 수학의 첫 단추입니다.

무슨 일을 한 걸까요?

브라우저 창 왼쪽 위를 기준으로 받은 client 좌표에서, 캔버스의 왼쪽 위 위치를 뺐습니다. 그러면 기준점이 바뀝니다.

localX = clientX - elementRect.left
localY = clientY - elementRect.top

이제 좌표는 “브라우저 창 기준”이 아니라 “캔버스 요소 기준”입니다. 이걸 local 좌표라고 부르겠습니다.

이 변환은 너무 쉬워서 오히려 중요해 보이지 않습니다. 하지만 앞으로 나올 모든 변환이 이 생각의 확장입니다.

어떤 좌표 = 원래 좌표 - 기준점

나중에는 여기에 zoom이 붙고, camera가 붙고, matrix inverse가 붙습니다. 이름은 점점 무서워지지만, 마음속으로는 계속 같은 질문을 하면 됩니다.

“지금 이 숫자는 어디 기준이지?”

page 좌표: 문서 전체 기준

pageX, pageY도 있습니다. 이건 문서 전체를 기준으로 한 좌표입니다. viewport 기준인 client 좌표에 scroll offset을 더한 값이라고 보면 됩니다.

pageX = clientX + window.scrollX
pageY = clientY + window.scrollY

일반적인 웹 UI에서는 page 좌표가 유용할 때가 있습니다. 문서 전체에서 툴팁을 띄운다든지, 스크롤 위치까지 포함해서 무언가를 배치한다든지요.

그런데 캔버스형 편집기에서는 보통 client에서 시작해서 필요한 요소의 rect를 빼는 방식이 더 깔끔합니다. 왜냐하면 편집기는 금방 복잡해지기 때문입니다. scroll container가 생기고, zoom layer가 생기고, overlay layer가 생깁니다. 이때 “문서 전체 기준” 하나로 밀어붙이면 나중에 머리가 아파집니다.

그래서 우리는 일단 이렇게 갑니다.

pointer event -> client coordinate
client coordinate - canvas rect -> canvas local coordinate

간단하죠. 간단할 때 잘 잡아야 합니다. 나중에 안 간단해집니다.

world 좌표: 편집기 문서의 좌표

아직 infinite canvas는 만들지 않았지만, 앞으로 계속 나올 world 좌표도 미리 얼굴만 보고 갑시다.

local 좌표는 어떤 요소 안에서의 좌표입니다. 예를 들어 캔버스 안에서 (120, 80) 같은 값입니다.

world 좌표는 편집기 문서 전체의 좌표입니다. Figma에서 화면을 왼쪽으로 밀고, 오른쪽으로 밀고, 확대하고, 축소해도 도형 자체의 문서상 위치는 변하지 않습니다. 그 문서상의 좌표를 world 좌표라고 부를 수 있습니다.

처음에는 이 정도로 구분하면 충분합니다.

client: 지금 보이는 브라우저 viewport 기준
page: 스크롤까지 포함한 문서 전체 기준
local: 특정 DOM 요소 내부 기준
world: 편집기 문서 전체 기준
screen: camera와 zoom이 적용된 최종 화면 기준

이번 강의에서는 client -> local까지만 다룹니다. 뒤에서 viewport와 camera를 만들면 이런 식이 등장합니다.

screen = (world - camera) * zoom
world = screen / zoom + camera

갑자기 공식이 나와도 겁먹을 필요 없습니다. 오늘 한 일과 같은 일입니다. 기준을 바꾸고, 크기를 조정하고, 다시 표현하는 겁니다.

데모에서 볼 것

데모 위에서 포인터를 움직여보면 client 좌표와 canvas local 좌표가 함께 표시됩니다.

여기서 볼 것은 숫자의 크기가 아닙니다. 숫자의 기준입니다.

같은 마우스 위치라도 브라우저 창의 왼쪽 위를 기준으로 보면 client 좌표입니다. 캔버스 영역의 왼쪽 위를 기준으로 다시 계산하면 local 좌표가 됩니다.

이 감각이 생기면 선택 박스가 이상한 곳에 그려질 때, ruler 숫자가 묘하게 안 맞을 때, hit testing이 엉뚱한 도형을 잡을 때 원인을 훨씬 빨리 찾을 수 있습니다.

그래픽 에디터의 많은 버그는 어려운 수학에서 오지 않습니다. 꽤 자주, 아주 평범한 좌표가 이름표 없이 돌아다니다가 사고를 칩니다. 오늘은 그 이름표를 붙이는 날입니다.

Edit this page
최근 수정: 26. 5. 14. PM 5:45
Contributors: easylogic
Next
점, 벡터, 거리와 기본 용어