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 길이 구하기

모션 수학과 timing 함수

부록으로 모션을 봅시다.

지금까지는 주로 “어디에 그릴 것인가?”를 다뤘습니다. 좌표, 행렬, viewport, selection, vector network까지 전부 공간의 문제였죠. 모션은 여기에 시간축을 하나 더 붙입니다.

geometry = position, size, rotation, color
motion = geometry가 시간에 따라 어떻게 바뀌는가

그래픽 에디터에서도 모션은 중요합니다. selection handle이 부드럽게 나타나고, canvas zoom이 튀지 않고, drag 후 snap guide가 적당히 사라지고, prototype transition을 미리 보여주려면 결국 timing을 다뤄야 합니다.

이제 칠판에 시간까지 올라왔습니다. 칠판이 점점 좁아지네요.

1. 모션은 시간을 progress로 바꾸는 일이다

가장 기본 모델은 이겁니다.

elapsed = now - startTime
progress = clamp(elapsed / duration, 0, 1)

progress는 항상 0..1 범위로 정규화합니다.

0 = animation start
1 = animation end

이제 시작 값과 끝 값을 보간합니다.

value = lerp(from, to, progress)

선형 보간은 우리가 gradient에서 본 공식과 같습니다.

lerp(a, b, t) = a + (b - a) * t

색도, 위치도, scale도, opacity도 같은 구조로 갈 수 있습니다.

x = lerp(x0, x1, p)
opacity = lerp(0, 1, p)
scale = lerp(0.96, 1, p)

그러니까 모션은 새로운 괴물이 아닙니다. 기존 수학에 시간을 하나 끼워 넣은 겁니다. 물론 괴물은 아니어도 가끔 성격은 있습니다.

2. timing 함수는 progress를 다시 매핑한다

linear animation은 속도가 일정합니다.

eased = progress

하지만 실제 UI에서는 linear가 딱딱하게 느껴질 때가 많습니다. 그래서 timing 함수를 씁니다.

eased = easing(progress)
value = lerp(from, to, eased)

즉 timing 함수는 값 자체를 바꾸는 함수가 아니라, 시간의 진행률을 다시 구부리는 함수입니다.

linear:
f(p) = p

easeInQuad:
f(p) = p^2

easeOutQuad:
f(p) = 1 - (1 - p)^2

easeInOutCubic:
if p < 0.5:
  f(p) = 4p^3
else:
  f(p) = 1 - (-2p + 2)^3 / 2

같은 시작점과 끝점이어도 timing 함수가 달라지면 속도감이 달라집니다.

position(t) = lerp(start, end, easing(t))
velocity(t) = derivative(position(t))

사용자는 보통 위치를 보는 것 같지만, 사실 속도의 변화를 많이 느낍니다. UI 모션에서 “부드럽다”는 말은 대체로 속도와 가속도가 갑자기 튀지 않는다는 뜻입니다.

3. CSS cubic-bezier는 시간-진행률 곡선이다

CSS transition에서 자주 보는 값입니다.

transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);

이건 cubic bezier curve입니다.

P0 = (0, 0)
P1 = (x1, y1)
P2 = (x2, y2)
P3 = (1, 1)

주의할 점이 있습니다. CSS cubic-bezier에서 x축은 시간이고 y축은 progress입니다.

x = time progress
y = eased progress

그래서 p가 들어오면 바로 bezier의 y를 읽는 게 아닙니다. 먼저 x(t) = p가 되는 curve parameter t를 찾고, 그 t에서 y(t)를 읽습니다.

given p:
  find u where bezierX(u) = p
  eased = bezierY(u)

브라우저가 이걸 대신 해줍니다. 우리가 스크립트 엔진을 만들면 이 계산을 직접 하거나, 충분히 근사해야 합니다.

4. spring은 목표값을 향해 감쇠하는 시스템이다

UI 모션에는 spring도 자주 씁니다.

간단한 감쇠 spring 느낌은 이렇게 근사할 수 있습니다.

f(p) = 1 - exp(-damping * p) * cos(frequency * p)

이 함수는 1을 향해 가면서 살짝 넘쳤다가 돌아옵니다.

overshoot -> settle

물리 spring을 조금 더 제대로 만들면 위치와 속도를 상태로 둡니다.

force = -stiffness * (x - target)
dampingForce = -damping * velocity
acceleration = force + dampingForce

velocity += acceleration * dt
x += velocity * dt

이건 시간 적분입니다. 여기서 dt는 frame 사이 시간입니다.

dt = (now - previousFrameTime) / 1000

CSS transition은 선언형이라 편하고, spring engine은 직접 상태를 다루니 더 유연합니다. 에디터의 pan inertia, snap animation, object settle 같은 건 spring 모델이 잘 어울립니다.

5. requestAnimationFrame으로 작은 엔진 만들기

스크립트 모션 엔진의 기본 루프는 이렇게 생겼습니다.

function tick(now) {
  const elapsed = now - startTime;
  const progress = clamp(elapsed / duration, 0, 1);
  const eased = easing(progress);
  const x = lerp(fromX, toX, eased);

  render(x);

  if (progress < 1) {
    requestAnimationFrame(tick);
  }
}

핵심은 render와 model update를 분리하는 겁니다.

time -> progress -> eased progress -> model value -> render

에디터에서는 더 중요합니다. DOM의 현재 위치를 읽어서 다음 위치를 만들면 금방 꼬입니다. model이 진짜 상태이고, DOM은 그 상태를 보여주는 projection이어야 합니다. 우리 강의 처음부터 계속 나온 말이죠. 교수님도 이제 거의 자동응답기입니다.

6. 엔진을 조금 더 제대로 쪼개기

단일 값 하나만 움직이면 엔진이랄 것도 없습니다. 실제 엔진은 보통 네 덩어리로 나눕니다.

Ticker
  requestAnimationFrame을 관리한다.

Player
  play, pause, cancel, finish 상태를 가진다.

Timeline
  여러 track의 delay, duration, easing을 관리한다.

Interpolator
  number, vector, color, transform 같은 값을 보간한다.

간단한 자료구조는 이렇게 잡을 수 있습니다.

type Track<T> = {
  property: string;
  from: T;
  to: T;
  delay: number;
  duration: number;
  easing: (p: number) => number;
  interpolate: (from: T, to: T, t: number) => T;
};

type Animation = {
  startTime: number;
  duration: number;
  tracks: Track<unknown>[];
  state: "idle" | "running" | "paused" | "finished";
};

매 frame마다 track별 progress를 따로 계산합니다.

globalProgress = clamp((now - startTime) / animationDuration, 0, 1)

trackProgress =
  clamp((elapsed - track.delay) / track.duration, 0, 1)

trackValue =
  interpolate(track.from, track.to, track.easing(trackProgress))

이렇게 하면 하나의 animation 안에서도 property마다 다르게 움직일 수 있습니다.

x       : delay 0ms,   duration 700ms
opacity : delay 0ms,   duration 220ms
scale   : delay 120ms, duration 360ms
rotate  : delay 180ms, duration 600ms
color   : delay 300ms, duration 400ms

데모의 Mode: timeline이 바로 이 모델입니다. 하나의 전체 progress가 있고, 그 안에서 x, scale, rotate, opacity, color가 각자 local progress를 가집니다.

global p = 0.50

x track:
  local p = 0.61

scale track:
  local p = 0.79

color track:
  local p = 0.28

여기서 중요한 건 progress를 한 번만 쓰지 않는다는 점입니다. timeline 엔진은 전체 시간과 track 시간을 분리합니다. 선생님이 출석을 한 번 부르지만 학생들은 각자 다른 속도로 움직이는 그런 느낌입니다. 반 전체가 산만합니다.

7. 값 타입마다 보간 방식이 다르다

number는 쉽습니다.

number = lerp(a, b, t)

2D point도 쉽습니다.

x = lerp(ax, bx, t)
y = lerp(ay, by, t)

색은 채널별 보간입니다.

r = lerp(r0, r1, t)
g = lerp(g0, g1, t)
b = lerp(b0, b1, t)
a = lerp(a0, a1, t)

transform은 살짝 조심해야 합니다. matrix 여섯 숫자를 그냥 보간하면 예상과 다른 shear나 scale이 끼어 보일 수 있습니다.

나쁜 경우:
matrixA와 matrixB의 a,b,c,d,e,f를 그대로 lerp

더 나은 경우:
translate, rotate, scale, skew로 분해
각 성분을 보간
다시 matrix로 합성

2D editor에서는 보통 이렇게 다룹니다.

position: vector lerp
rotation: shortest-angle lerp
scale: positive number lerp
opacity: clamp(lerp)
color: color space policy를 정해서 lerp

회전은 특히 350deg -> 10deg가 문제입니다.

naive delta = 10 - 350 = -340deg
shortest delta = +20deg

그래서 각도 보간은 shortest path를 씁니다.

delta = ((to - from + 180) mod 360) - 180
angle = from + delta * t

작은 차이 같지만, UI가 갑자기 반 바퀴 돌아가면 사용자는 “왜 애가 갑자기 체조를 하지?” 하게 됩니다.

8. interrupt와 cancel이 엔진의 진짜 실전이다

모션 엔진은 재생보다 중단이 더 어렵습니다.

play는 쉽다.
interrupt가 실전이다.

에디터에서는 사용자가 애니메이션 도중에 바로 다시 조작합니다.

snap animation 중 다시 drag
zoom animation 중 wheel 입력
selection fade 중 다른 node 선택
prototype transition 중 back action

이때 정책이 필요합니다.

cancel:
  현재 animation을 즉시 중단하고 새 입력을 적용한다.

retarget:
  현재 값과 속도를 새 animation의 시작값으로 삼는다.

finish:
  현재 animation을 끝 상태로 보낸 뒤 다음 animation을 시작한다.

에디터 조작에서는 대체로 cancel 또는 retarget이 좋습니다. 사용자의 직접 조작이 항상 우선입니다.

function interrupt(animation, nextTarget) {
  const current = animation.sample(performance.now());
  animation.cancel();
  return animate({
    from: current,
    to: nextTarget,
    preserveVelocity: true
  });
}

spring 엔진에서는 preserveVelocity가 중요합니다. 속도를 0으로 리셋하면 움직임이 툭 끊깁니다.

9. CSS 모션으로 어디까지 할 수 있나?

CSS 모션은 생각보다 멀리 갑니다. MDN 기준으로 CSS transitions는 property 변경을 duration, delay, easing으로 보간할 수 있고, CSS animations는 @keyframes로 여러 구간을 선언할 수 있습니다. Web Animations API의 Element.animate()는 브라우저 animation engine을 JS에서 직접 다루는 방법입니다.

CSS만으로 잘 되는 영역은 이렇습니다.

hover/focus feedback
button press
tooltip/popover fade
panel slide
selection outline fade
toast enter/exit
simple loading indicator
state A -> state B transition
scroll-driven visual progress

주로 좋은 property는 이쪽입니다.

transform
opacity
filter
color/background-color
box-shadow
clip-path 일부
registered custom property

특히 transform과 opacity는 layout을 다시 풀지 않아도 되는 경우가 많아서 UI 모션의 기본 재료로 좋습니다.

.node {
  transition:
    transform 180ms cubic-bezier(0.2, 0, 0, 1),
    opacity 120ms linear;
}

.node[data-selected="true"] {
  transform: translateY(-2px) scale(1.02);
  opacity: 1;
}

@keyframes는 반복되거나 중간 상태가 있는 모션에 좋습니다.

@keyframes pulse {
  0% { transform: scale(1); opacity: 0.72; }
  50% { transform: scale(1.08); opacity: 1; }
  100% { transform: scale(1); opacity: 0.72; }
}

.cursor-ring {
  animation: pulse 900ms ease-in-out infinite;
}

registered custom property를 쓰면 CSS 변수도 타입을 갖고 보간될 수 있습니다.

@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.dial {
  transition: --angle 240ms ease;
  transform: rotate(var(--angle));
}

이건 gradient, mask, transform helper 값을 CSS 변수로 움직일 때 유용합니다. 그냥 --angle만 쓰면 브라우저가 중간값 타입을 모를 수 있는데, @property로 등록하면 보간 가능한 값이 됩니다.

Scroll-driven animation도 CSS 영역입니다. 스크롤 위치를 timeline으로 삼아 animation progress를 만들 수 있습니다.

scroll position -> animation progress

문서형 강의 페이지, progress indicator, section reveal에는 좋습니다. 다만 그래픽 에디터의 camera나 selection model처럼 앱 상태와 강하게 묶인 모션은 JS 쪽이 더 명확합니다.

10. CSS 모션의 한계

CSS가 모든 모션을 해결하지는 않습니다.

어려운 영역은 이렇습니다.

drag 중 포인터를 정확히 따라가는 motion
physics spring with velocity
camera inertia
multi-object choreography
timeline scrubber
undo/redo와 연결된 model animation
canvas/WebGL object animation
collision, constraints, snapping과 결합된 motion

CSS는 선언형이라 “상태가 바뀌면 브라우저가 알아서 보간”하는 데 강합니다. 반대로 매 frame 모델 값을 계산하고, 사용자의 입력으로 언제든 중단하고, 여러 오브젝트의 시간을 맞춰야 하면 JS 엔진이 필요합니다.

또 layout property는 조심해야 합니다.

width, height, top, left, margin

이런 값도 애니메이션이 가능할 수 있지만 layout을 다시 계산하게 만들 수 있습니다. 에디터 canvas 위 오브젝트라면 가능하면 transform으로 움직이는 편이 낫습니다.

권장:
transform: translate(...) scale(...) rotate(...)

주의:
left/top/width/height를 매 frame 변경

그리고 auto는 보간하기 까다롭습니다.

height: 0 -> height: auto

이런 모션은 CSS만으로 깔끔하지 않을 때가 많습니다. content height를 측정해서 px로 넣거나, grid trick, clip, transform scale 같은 우회가 필요합니다.

11. CSS, WAAPI, JS 엔진 선택 기준

정리하면 이렇게 나눌 수 있습니다.

방식좋은 곳약한 곳
CSS transition상태 A/B 전환, hover/focus, 단순 UI feedback세밀한 중단/재타겟, 물리 기반 motion
CSS keyframes반복, 로딩, 정해진 시퀀스앱 model과 동기화되는 timeline
CSS scroll-driven animation문서/스크롤 기반 progresseditor camera 같은 내부 상태 기반 progress
WAAPI Element.animate()JS에서 브라우저 animation player 제어복잡한 물리/geometry engine
custom JS engineeditor model, physics, multi-object timeline직접 최적화와 lifecycle 관리 필요

실무적으로는 이렇게 잡으면 좋습니다.

DOM UI chrome:
  CSS transition / keyframes

단일 element animation을 JS에서 제어:
  Web Animations API

editor canvas model:
  custom JS motion engine

Canvas/WebGL renderer:
  render loop 안에서 직접 time integration

12. editor motion에서 조심할 것

에디터 모션은 예쁘기만 하면 안 됩니다. 조작감이 먼저입니다.

drag 중:
  사용자 입력이 우선이다.
  느린 easing으로 포인터를 따라오면 안 된다.

drag 끝:
  snap, inertia, settle animation을 적용할 수 있다.

zoom:
  cursor anchored zoom 공식을 깨면 안 된다.

selection:
  overlay는 geometry model을 늦게 따라오면 안 된다.

즉 모션은 정보 구조를 흐리면 안 됩니다. 특히 Figma 같은 도구에서는 사용자가 “내가 잡은 점이 어디 있는지”를 즉시 믿을 수 있어야 합니다.

좋은 기준은 이렇습니다.

direct manipulation -> 즉시 반응
state transition    -> 짧고 명확한 easing
decorative feedback -> interrupt 가능
camera motion       -> 좌표 보존 공식 우선

마무리

모션 수학의 핵심은 간단합니다.

1. 시간을 progress로 정규화한다.
2. timing 함수로 progress를 다시 매핑한다.
3. 보간으로 값을 만든다.
4. requestAnimationFrame에서 매 frame 렌더링한다.

이 구조를 알면 CSS transition도, cubic-bezier도, spring도 같은 가족으로 보입니다.

그래픽 에디터에서 모션은 장식이 아닙니다. 사용자가 geometry 변화를 이해하도록 돕는 시간축의 시각화입니다. 예쁘게 움직이는 것도 좋지만, 먼저 정확히 움직여야 합니다. 예쁜데 틀리면 그건 그냥 자신감 있는 버그입니다.

Edit this page
최근 수정: 26. 5. 14. PM 5:45
Contributors: easylogic
Next
Keyframe timeline 엔진