designpaca 초기 구현 — 스킬 · 설치 CLI · 배포 파이프라인
웹 디자인 파이프라인 스킬과 이를 5개 에이전트에 설치하는 CLI 를 담은 모노레포. 스킬 (packages/skill) - SKILL.md 261줄 + 참조 문서 16개 3,349줄. progressive disclosure 로 본문은 절차와 인덱스만, 지식은 references/ 로 분리 - 0~6단계 파이프라인. 규모에 따라 전체·연장·국소 세 경로로 분기 - 하드 게이트 12개는 grep·카운트로 검증 가능한 것만. 취향 판단은 제외 - 미학 프리셋 5종, AI 슬롭 지문 목록, 한글 조판 규칙, SVG 필터·three.js·인터랙티브 모션·HTML-in-Canvas 실전 지침 설치 CLI (packages/cli, packages/core) - npx designpaca 온보딩 TUI. Claude Code · Codex · Cursor · Windsurf · AGENTS.md - 매니페스트에 설치 시점 해시를 기록해 사용자가 고친 파일은 update 가 건너뛴다 - 타깃별로 본문의 references/ 경로를 실제 설치 위치로 재작성 - AGENTS.md 는 항상 로드되므로 본문 대신 303자 포인터만 주입 - Windsurf 는 12,000자 상한 초과 시 설치를 차단 배포 (build/ci, .forgejo/workflows) - 태그 v* → 검사·테스트·빌드 → npmjs 배포 + Forgejo 레지스트리 미러 → draft 릴리스 → Cloudflare Pages. 재실행 멱등 근거 (research/) - 약 250개 웹 소스 조사 결과와 도그푸딩 검증 2건. 스킬의 모든 수치는 여기서 나온다 테스트 22개 통과 (core 16 · cli 6)
This commit is contained in:
commit
8808c672dc
135 changed files with 38838 additions and 0 deletions
552
research/motion/01-principles.md
Normal file
552
research/motion/01-principles.md
Normal file
|
|
@ -0,0 +1,552 @@
|
|||
# 01. 모션 원칙 — 왜 이 움직임인가
|
||||
|
||||
> 이 문서의 목적: 에이전트가 "어떤 이징을, 몇 ms로, 어떤 순서로" 쓸지를 **취향이 아니라 규칙으로** 결정하게 만든다.
|
||||
> 모션은 장식이 아니라 **인터페이스의 문법**이다. 문법에는 옳고 그름이 있다.
|
||||
|
||||
---
|
||||
|
||||
## 0. 대전제: 모션은 4가지 일만 한다
|
||||
|
||||
애니메이션을 넣기 전에 반드시 아래 4개 중 **어느 것에 해당하는지 한 문장으로** 답할 수 있어야 한다.
|
||||
답할 수 없으면 그 애니메이션은 넣지 않는다.
|
||||
|
||||
| # | 역할 | 질문 | 예시 |
|
||||
|---|---|---|---|
|
||||
| 1 | **인과(causality)** | "이게 왜 여기 나타났나?"에 답하는가 | 버튼을 누른 지점에서 시트가 자라남 |
|
||||
| 2 | **공간 연속성(continuity)** | "이 요소가 어디서 와서 어디로 갔나?"에 답하는가 | 썸네일 → 상세 이미지로 이어지는 shared element |
|
||||
| 3 | **상태 변화 고지(feedback)** | "내 입력이 접수됐나?"에 답하는가 | 프레스 시 눌림, 토글 스위치, 검증 실패 표시 |
|
||||
| 4 | **주의 유도(attention)** | "지금 어디를 봐야 하나?"에 답하는가 | 새 항목 하이라이트, 에러 필드로의 스크롤 |
|
||||
|
||||
**여기에 없는 다섯 번째, "멋있어서"는 정당한 이유가 아니다.** 단, 브랜드 표현이 명시적 요구사항인
|
||||
히어로/랜딩/포트폴리오에서는 5번 **연출(expression)** 이 추가로 허용된다. 대신 연출 모션은
|
||||
반드시 (a) 스크롤/호버로 사용자가 통제 가능하거나 (b) 1회성이며 (c) `prefers-reduced-motion`에서
|
||||
완전히 제거 가능해야 한다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 이징의 의미론 — 곡선은 "물리적 인상"이다
|
||||
|
||||
이징 곡선은 속도의 시간 변화율이다. 사람의 뇌는 이것을 **무게, 힘, 의도**로 읽는다.
|
||||
|
||||
### 1.1 기본 곡선 표
|
||||
|
||||
| 이징 | cubic-bezier | 속도 변화 | 심리적 인상 | 언제 쓰나 | 쓰면 안 되는 곳 |
|
||||
|---|---|---|---|---|---|
|
||||
| `linear` | `(0,0,1,1)` | 일정 | 기계적, 무생물 | 무한 회전(스피너), 마퀴, **스크롤 스크럽** | 위치 이동, 등장/퇴장 |
|
||||
| `ease-out` | `(0,0,0.58,1)` | 빠르게 시작 → 감속 | "도착했다", 안정감 | **화면에 들어오는 모든 것**. 진입, 열림, 확장 | 화면 밖으로 나가는 것 |
|
||||
| `ease-in` | `(0.42,0,1,1)` | 느리게 시작 → 가속 | "떠난다", 이탈 | **화면 밖으로 나가는 것**. 퇴장, 닫힘 | 화면에 남는 것 (끝이 뚝 끊겨 어색함) |
|
||||
| `ease-in-out` | `(0.42,0,0.58,1)` | 감속-가속 대칭 | 신중함, 왕복 | 화면 안에서의 **이동**(A→B), 루프 애니메이션 | 짧은 마이크로 인터랙션(굼떠 보임) |
|
||||
| `ease`(기본값) | `(0.25,0.1,0.25,1)` | 짧은 가속 → 긴 감속 | 무난함 | 딱히 고민할 여유가 없을 때의 기본값 | 프로덕션 최종 튜닝 |
|
||||
| **spring** | (곡선 아님) | 물리 시뮬레이션 | 생동감, 실체감, 손맛 | 직접 조작(드래그·제스처), 토글, 레이아웃 변화 | 색/투명도 변화, 텍스트 |
|
||||
|
||||
> **핵심 규칙 (Josh Comeau, Material, Carbon이 모두 동의하는 지점)**
|
||||
> **들어오는 것은 감속(ease-out), 나가는 것은 가속(ease-in), 안에서 움직이는 것은 감속-가속(ease-in-out).**
|
||||
> 이유: 화면에 남을 요소가 급정거하면 충돌처럼 보이고, 화면을 떠날 요소가 느리게 사라지면
|
||||
> "왜 아직 안 갔지"라는 대기 비용이 생긴다.
|
||||
|
||||
### 1.2 왜 `linear`는 거의 항상 틀린가
|
||||
|
||||
현실의 모든 물체는 질량이 있어 가속·감속한다. 등속 운동은 뇌가 "이건 물체가 아니라 그려진 것"으로
|
||||
읽는다. 예외는 **애초에 물체가 아닌 것**뿐이다: 진행률 표시, 회전 로더, 무한 마퀴,
|
||||
그리고 **스크롤에 직접 물린 애니메이션**(사용자의 스크롤 자체가 이미 이징이므로 곡선을 덧씌우면 이중 이징이 된다).
|
||||
|
||||
### 1.3 커스텀 cubic-bezier를 만들 때의 안전 규칙
|
||||
|
||||
- `x1`, `x2`는 반드시 `0~1` 범위. 벗어나면 무효.
|
||||
- `y1`, `y2`는 `0~1`을 벗어나도 되며 이때 **오버슛/언더슛**이 생긴다 (예: `cubic-bezier(0.34, 1.56, 0.64, 1)` = back-out).
|
||||
- 오버슛은 **작고 가벼운 요소**(칩, 아이콘, 토스트)에만. 큰 면적(모달, 시트)이 오버슛하면 멀미를 유발한다.
|
||||
- 오버슛 폭은 이동 거리의 **5~10%**를 넘기지 않는다.
|
||||
|
||||
```css
|
||||
/* 프로덕션에서 실제로 쓸 만한 커스텀 곡선 세트 */
|
||||
--ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
--ease-out-cubic: cubic-bezier(0.22, 0.61, 0.36, 1);
|
||||
--ease-out-quart: cubic-bezier(0.16, 1, 0.3, 1); /* 가장 범용적인 "고급스러운" 진입 */
|
||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-in-quart: cubic-bezier(0.5, 0, 0.75, 0); /* 퇴장 */
|
||||
--ease-in-out-quart:cubic-bezier(0.76, 0, 0.24, 1); /* 화면 내 이동 */
|
||||
--ease-back-out: cubic-bezier(0.34, 1.56, 0.64, 1); /* 작은 요소 전용 오버슛 */
|
||||
--ease-anticipate: cubic-bezier(0.68, -0.6, 0.32, 1.6); /* 뒤로 당겼다 튀어나감 — 아주 아껴 쓸 것 */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 스프링 — 언제 곡선 대신 물리를 쓰나
|
||||
|
||||
### 2.1 스프링이 이기는 상황
|
||||
|
||||
스프링은 **목표 지점이 도중에 바뀔 수 있는 애니메이션**에서 압도적으로 유리하다.
|
||||
duration 기반 이징은 목표가 바뀌면 처음부터 다시 시작하거나 속도가 뚝 끊기지만,
|
||||
스프링은 **현재 속도(velocity)를 이어받아** 자연스럽게 궤도를 수정한다.
|
||||
|
||||
| 상황 | 선택 |
|
||||
|---|---|
|
||||
| 드래그를 놓았을 때의 관성/스냅 | **스프링** (필수) |
|
||||
| 커서를 따라가는 요소 | **스프링** 또는 lerp |
|
||||
| 토글 스위치, 체크박스 | **스프링** (손맛) |
|
||||
| 레이아웃 변화(FLIP) | **스프링** |
|
||||
| 모달 진입/퇴장 | 곡선 (예측 가능한 지속시간이 더 중요) |
|
||||
| 색상·투명도 변화 | **곡선** (스프링의 바운스가 색에는 무의미하고 깜빡임처럼 보임) |
|
||||
| 스크롤 스크럽 | **둘 다 아님** (linear) |
|
||||
|
||||
### 2.2 파라미터의 의미
|
||||
|
||||
| 파라미터 | 별칭 | 올리면 | 실무 기본값 |
|
||||
|---|---|---|---|
|
||||
| `stiffness` | tension | 더 빠르고 날카롭게 | 170~400 |
|
||||
| `damping` | friction | 바운스가 줄고 차분해짐 | 20~40 |
|
||||
| `mass` | — | 무겁고 굼뜨게, 관성 증가 | 1 (거의 안 건드림) |
|
||||
|
||||
**감쇠비(ζ) = damping / (2 × √(stiffness × mass))**
|
||||
|
||||
- ζ < 1 → 언더댐프(바운스 있음)
|
||||
- ζ = 1 → 임계 감쇠(바운스 없이 최단 시간에 정착) ← **UI 기본값으로 가장 안전**
|
||||
- ζ > 1 → 오버댐프(느리게 미끄러짐)
|
||||
|
||||
> Motion 라이브러리는 `stiffness/damping` 대신 **`bounce`(0~1) + `visualDuration`** 조합을 권장한다.
|
||||
> 이게 디자이너 친화적이다: `{ type: "spring", visualDuration: 0.35, bounce: 0.2 }`
|
||||
> `bounce: 0`이면 임계 감쇠와 동일, `bounce: 0.25`가 Motion 기본값.
|
||||
|
||||
### 2.3 CSS에서 스프링 흉내내기 — `linear()` 프리셋 (직접 계산한 값)
|
||||
|
||||
CSS에는 스프링이 없지만 `linear()`(Baseline Widely available, 2023-12)로 근사할 수 있다.
|
||||
아래는 실제 스프링 미분방정식 `x(t) = 1 − e^(−ζω₀t)(cos(ω_d t) + (ζω₀/ω_d)sin(ω_d t))` 를
|
||||
샘플링해 생성한 값이다. 주석의 `duration`을 **반드시 함께** 써야 의도한 물리가 나온다.
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* soft — m=1 k=170 d=26, ζ=0.997, 바운스 없음 / duration: 700ms */
|
||||
--spring-soft: linear(
|
||||
0, 0.0039 1%, 0.0148 2%, 0.0314 3%, 0.0526 4%, 0.1053 6%, 0.4302 16%,
|
||||
0.5189 19%, 0.598 22%, 0.6669 25%, 0.7259 28%, 0.7907 32%, 0.8415 36%,
|
||||
0.8971 42%, 0.9474 51%, 1
|
||||
);
|
||||
|
||||
/* snappy — m=1 k=300 d=30, ζ=0.866, 거의 바운스 없음 / duration: 505ms */
|
||||
--spring-snappy: linear(
|
||||
0, 0.0036 1%, 0.0138 2%, 0.0296 3%, 0.05 4%, 0.0743 5%, 0.1317 7%,
|
||||
0.4735 17%, 0.5669 20%, 0.65 23%, 0.7221 26%, 0.7831 29%, 0.8337 32%,
|
||||
0.8869 36%, 0.9342 41%, 0.9739 48%, 1
|
||||
);
|
||||
|
||||
/* bouncy — m=1 k=400 d=20, ζ=0.5, 최대 16% 오버슛 / duration: 635ms */
|
||||
--spring-bouncy: linear(
|
||||
0, 0.0077 1%, 0.0295 2%, 0.0634 3%, 0.1074 4%, 0.1597 5%, 0.2185 6%,
|
||||
0.282 7%, 0.6231 12%, 0.6881 13%, 0.75 14%, 0.8084 15%, 0.8627 16%,
|
||||
0.9126 17%, 0.9579 18%, 0.9984 19%, 1.0341 20%, 1.065 21%, 1.0913 22%,
|
||||
1.113 23%, 1.1305 24%, 1.1438 25%, 1.1535 26%, 1.1596 27%, 1.1628 29%,
|
||||
1.156 31%, 1.1329 34%, 1.0273 44%, 0.9976 48%, 0.9777 53%, 1
|
||||
);
|
||||
|
||||
/* gentle — m=1 k=120 d=14, ζ=0.639, 7% 오버슛 / duration: 940ms */
|
||||
--spring-gentle: linear(
|
||||
0, 0.0051 1%, 0.0195 2%, 0.0419 3%, 0.071 4%, 0.1058 5%, 0.1453 6%,
|
||||
0.2342 8%, 0.528 14%, 0.6206 16%, 0.7058 18%, 0.7823 20%, 0.8492 22%,
|
||||
0.9062 24%, 0.9534 26%, 0.9915 28%, 1.021 30%, 1.0429 32%, 1.0636 35%,
|
||||
1.0725 38%, 1.0673 44%, 1.0014 67%, 1
|
||||
);
|
||||
|
||||
/* nobounce — m=1 k=200 d=28.284, ζ=1.0 (임계 감쇠) / duration: 650ms */
|
||||
--spring-nobounce: linear(
|
||||
0, 0.004 1%, 0.0151 2%, 0.0319 3%, 0.0534 4%, 0.1068 6%, 0.4339 16%,
|
||||
0.5227 19%, 0.6017 22%, 0.6703 25%, 0.7291 28%, 0.7934 32%, 0.8438 36%,
|
||||
0.8986 42%, 0.9483 51%, 1
|
||||
);
|
||||
}
|
||||
|
||||
/* 사용 예 */
|
||||
.sheet {
|
||||
transition: translate 700ms var(--spring-soft);
|
||||
}
|
||||
.chip {
|
||||
transition: scale 635ms var(--spring-bouncy);
|
||||
}
|
||||
```
|
||||
|
||||
> **주의**: `linear()`로 오버슛(값 > 1)을 만들려면 애니메이션되는 값 자체가 오버슛을 허용해야 한다.
|
||||
> `opacity`에 `--spring-bouncy`를 쓰면 1을 넘는 구간이 클램프되어 정체 구간이 생긴다. **transform 계열에만.**
|
||||
|
||||
---
|
||||
|
||||
## 3. 지속시간 — 숫자로 못 박기
|
||||
|
||||
### 3.1 인지 기준선
|
||||
|
||||
| 임계값 | 의미 | 근거 |
|
||||
|---|---|---|
|
||||
| **~100ms** | 사용자가 "즉각적"이라고 느끼는 한계. 직접 조작의 착각이 유지됨 | NN/g |
|
||||
| **~1s** | 사고 흐름이 끊기지 않는 한계 | NN/g |
|
||||
| **>400ms** | UI 애니메이션은 여기서부터 "느리다"고 인식되기 시작 | NN/g |
|
||||
| **>500ms** | "짜증난다(a real drag)" 구간 진입 | NN/g |
|
||||
| **200ms** | INP(Interaction to Next Paint) "good" 임계값 | Core Web Vitals |
|
||||
|
||||
> NN/g 요약: **대부분의 UI 애니메이션은 100~500ms.** 애니메이션은 "너무 짧아서" 문제가 되는 경우보다
|
||||
> **"너무 길어서" 문제가 되는 경우가 압도적으로 많다.**
|
||||
|
||||
### 3.2 designpaca 표준 지속시간 표 ← **이 표를 기본값으로 쓴다**
|
||||
|
||||
| 카테고리 | 지속시간 | 이징 | 예시 |
|
||||
|---|---|---|---|
|
||||
| **즉각 피드백** | 60~100ms | `ease-out` | 버튼 프레스, 체크박스, 리플 시작, 아이콘 상태 |
|
||||
| **호버/포커스** | 120~180ms | `ease-out` (진입) / 240ms `ease` (이탈) | 링크 밑줄, 카드 리프트, 컬러 변화 |
|
||||
| **작은 컴포넌트 진입** | 180~250ms | `ease-out-quart` | 툴팁, 드롭다운, 토스트, 칩 |
|
||||
| **작은 컴포넌트 퇴장** | 120~180ms | `ease-in` | 위와 동일 (진입의 **0.6~0.75배**) |
|
||||
| **중간 표면 진입** | 250~350ms | `ease-out-quart` 또는 spring(nobounce) | 모달, 사이드 시트, 아코디언 |
|
||||
| **중간 표면 퇴장** | 180~250ms | `ease-in-quart` | 위와 동일 |
|
||||
| **전체 화면 전환** | 300~450ms | `ease-in-out-quart` | 페이지 전환, 풀스크린 확장 |
|
||||
| **shared element 전환** | 350~500ms | `ease-in-out-quart` | 썸네일 → 상세 |
|
||||
| **스크롤 리빌** | 400~700ms | `ease-out-quart` | 섹션 등장 (1회성) |
|
||||
| **앰비언트 / 루프** | 3~20s | `ease-in-out` 또는 `linear` | 배경 그라디언트 드리프트, 마퀴 |
|
||||
| **스크롤 스크럽** | (해당 없음) | `linear` / `none` | 스크롤 진행률에 직접 물린 것 |
|
||||
|
||||
### 3.3 지속시간 조정 규칙 (표에서 벗어날 때)
|
||||
|
||||
1. **거리 비례**: 이동 거리가 2배면 지속시간은 **√2배** 정도만 늘린다 (선형 비례 아님).
|
||||
IBM Carbon이 명시: "요소의 이동 거리·크기가 클수록 애니메이션은 길어져야 한다."
|
||||
실무 근사: `duration = base × (distance / baseDistance)^0.5`, 상한은 항상 위 표의 최대값.
|
||||
2. **진입 > 퇴장**: 퇴장은 진입의 **0.6~0.75배**. 나가는 것은 볼 이유가 없다.
|
||||
3. **면적 비례**: 화면 면적을 많이 차지할수록 길게. 단 500ms 상한.
|
||||
4. **반복 빈도 반비례**: 사용자가 하루에 100번 보는 애니메이션은 **가장 짧게**. 1번 보는 것은 길어도 된다.
|
||||
→ 검색 결과 필터링(빈번) 150ms vs 온보딩 일러스트(1회) 800ms.
|
||||
5. **모바일은 짧게**: 터치는 마우스보다 직접적이라 지연에 더 민감하다. 데스크톱 값의 0.8배.
|
||||
|
||||
### 3.4 정립된 시스템의 실제 토큰값 (1차 출처 확인)
|
||||
|
||||
#### Material Design 3 (`material-web` 리포지토리 `_md-sys-motion.scss`, v0.192)
|
||||
|
||||
```css
|
||||
/* Easing */
|
||||
--md-sys-motion-easing-linear: cubic-bezier(0, 0, 1, 1);
|
||||
--md-sys-motion-easing-standard: cubic-bezier(0.2, 0, 0, 1);
|
||||
--md-sys-motion-easing-standard-accelerate: cubic-bezier(0.3, 0, 1, 1);
|
||||
--md-sys-motion-easing-standard-decelerate: cubic-bezier(0, 0, 0, 1);
|
||||
--md-sys-motion-easing-emphasized: cubic-bezier(0.2, 0, 0, 1);
|
||||
--md-sys-motion-easing-emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15);
|
||||
--md-sys-motion-easing-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1);
|
||||
--md-sys-motion-easing-legacy: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--md-sys-motion-easing-legacy-accelerate: cubic-bezier(0.4, 0, 1, 1);
|
||||
--md-sys-motion-easing-legacy-decelerate: cubic-bezier(0, 0, 0.2, 1);
|
||||
|
||||
/* Duration */
|
||||
--md-sys-motion-duration-short1: 50ms;
|
||||
--md-sys-motion-duration-short2: 100ms;
|
||||
--md-sys-motion-duration-short3: 150ms;
|
||||
--md-sys-motion-duration-short4: 200ms;
|
||||
--md-sys-motion-duration-medium1: 250ms;
|
||||
--md-sys-motion-duration-medium2: 300ms;
|
||||
--md-sys-motion-duration-medium3: 350ms;
|
||||
--md-sys-motion-duration-medium4: 400ms;
|
||||
--md-sys-motion-duration-long1: 450ms;
|
||||
--md-sys-motion-duration-long2: 500ms;
|
||||
--md-sys-motion-duration-long3: 550ms;
|
||||
--md-sys-motion-duration-long4: 600ms;
|
||||
--md-sys-motion-duration-extra-long1: 700ms;
|
||||
--md-sys-motion-duration-extra-long2: 800ms;
|
||||
--md-sys-motion-duration-extra-long3: 900ms;
|
||||
--md-sys-motion-duration-extra-long4:1000ms;
|
||||
```
|
||||
|
||||
M3의 의미론:
|
||||
- **decelerate** = 화면 밖에서 들어와 화면에 멈추는 것(**enter**)
|
||||
- **accelerate** = 화면에 있던 것이 멈추지 않고 화면 밖으로 나가는 것(**exit**)
|
||||
- **emphasized** = 사용자의 주의를 끌어야 하는 큰 전환. `standard`보다 초반이 더 느리고 후반이 더 빠른 비대칭 곡선
|
||||
- **standard** = 화면 안에서 시작하고 끝나는 대부분의 전환
|
||||
- **legacy** = M2 시절 곡선. 신규 프로젝트에서는 쓰지 않는다
|
||||
|
||||
> **주의**: M3 사양의 "emphasized"는 원래 **2개 곡선을 이어붙인 3점 곡선**이라 단일 cubic-bezier로
|
||||
> 정확히 표현되지 않는다. 위 토큰값은 웹용 근사치다. 정확히 재현하려면 `linear()`를 쓰거나
|
||||
> JS 애니메이션에서 키프레임 2개로 나눈다.
|
||||
|
||||
#### IBM Carbon (`@carbon/motion` v11.50.0 패키지 소스에서 직접 확인)
|
||||
|
||||
```css
|
||||
/* productive — 실무 작업용. 빠르고 방해되지 않음 */
|
||||
--carbon-ease-standard-productive: cubic-bezier(0.2, 0, 0.38, 0.9);
|
||||
--carbon-ease-entrance-productive: cubic-bezier(0, 0, 0.38, 0.9);
|
||||
--carbon-ease-exit-productive: cubic-bezier(0.2, 0, 1, 0.9);
|
||||
|
||||
/* expressive — 브랜드/감성 표현용. 크고 눈에 띄는 전환 */
|
||||
--carbon-ease-standard-expressive: cubic-bezier(0.4, 0.14, 0.3, 1);
|
||||
--carbon-ease-entrance-expressive: cubic-bezier(0, 0, 0.3, 1);
|
||||
--carbon-ease-exit-expressive: cubic-bezier(0.4, 0.14, 1, 1);
|
||||
|
||||
/* duration — 용도가 토큰 이름에 직접 문서화되어 있다 */
|
||||
--carbon-duration-fast-01: 70ms; /* 버튼·토글 등 마이크로 인터랙션. 즉각 반응 */
|
||||
--carbon-duration-fast-02: 110ms; /* 작은 UI 요소의 미묘한 진입/퇴장(페이드) */
|
||||
--carbon-duration-moderate-01: 150ms; /* 작은 확장, 짧은 거리 이동. 기본 전환 속도 */
|
||||
--carbon-duration-moderate-02: 240ms; /* 확장, 시스템 알림, 토스트 */
|
||||
--carbon-duration-slow-01: 400ms; /* 큰 확장, 중요 시스템 알림 */
|
||||
--carbon-duration-slow-02: 700ms; /* 배경 디밍, 대형 히어로 전환 */
|
||||
```
|
||||
|
||||
Carbon의 핵심 통찰: **하나의 제품 안에 두 개의 모션 인격이 공존한다.**
|
||||
- 데이터 테이블, 폼, 툴바 = **productive** (짧고 절제됨, 70~240ms)
|
||||
- 온보딩, 마케팅 섹션, 빈 상태 = **expressive** (400~700ms)
|
||||
같은 제품에서 이 둘을 섞지 말고, **컴포넌트 성격에 따라 결정**한다.
|
||||
|
||||
#### Apple HIG
|
||||
|
||||
Apple은 수치 토큰을 공개하지 않고 원칙만 제시한다. 요약:
|
||||
- 모션은 **사용자의 작업을 돕는 목적**일 때만 쓴다
|
||||
- **일관성**: 같은 종류의 전환은 항상 같은 모션
|
||||
- **Reduce Motion** 설정을 반드시 존중한다. 시스템 제공 애니메이션은 자동 대체되지만 커스텀은 직접 처리
|
||||
- iOS/macOS의 기본 감성은 **스프링**이다 (SwiftUI의 기본 애니메이션이 스프링)
|
||||
- 화면 전체가 회전·확대·시차 이동하는 효과는 멀미 유발원 1순위
|
||||
|
||||
---
|
||||
|
||||
## 4. Stagger와 오케스트레이션
|
||||
|
||||
### 4.1 stagger 규칙
|
||||
|
||||
| 항목 수 | 항목당 지연 | 총 지속시간 상한 |
|
||||
|---|---|---|
|
||||
| 3~5개 (네비게이션) | 40~60ms | 300ms |
|
||||
| 6~12개 (카드 그리드) | 30~50ms | 500ms |
|
||||
| 12~30개 (리스트) | 15~25ms | 600ms |
|
||||
| 30개 초과 | **stagger 쓰지 않음** — 첫 화면 항목만 stagger, 나머지는 동시 | — |
|
||||
|
||||
**절대 규칙**
|
||||
1. `총 지속시간 = (항목수 − 1) × stagger + 개별 duration` 이 **800ms를 넘으면 안 된다.**
|
||||
100개 리스트에 20ms stagger를 걸면 2초가 걸린다. 그건 기능이 아니라 버그다.
|
||||
2. stagger는 **가상 스크롤/무한 스크롤에서는 첫 페이지에만** 적용한다. 스크롤할 때마다 stagger가
|
||||
재생되면 읽기를 방해한다.
|
||||
3. stagger 방향은 **읽기 방향**과 일치시킨다 (좌→우, 상→하). 중앙에서 퍼지는 stagger는
|
||||
그리드/방사형 레이아웃에만.
|
||||
|
||||
### 4.2 오케스트레이션 — 순서의 문법
|
||||
|
||||
Disney 12원칙 중 UI에 실제로 유효한 4가지:
|
||||
|
||||
| 원칙 | UI 적용 |
|
||||
|---|---|
|
||||
| **Slow in / Slow out** (이징) | 위 1장 전체 |
|
||||
| **Timing** | 위 3장 전체 |
|
||||
| **Follow through / Overlapping action** | 연관 요소들이 **동시에 끝나지 않게** 한다. 카드가 먼저, 그 안의 텍스트가 60ms 뒤에. 이게 "고급스러움"의 정체다 |
|
||||
| **Anticipation** | 큰 동작 전 반대 방향으로 살짝 당긴다. **아껴 쓸 것** — 지연으로 느껴지면 실패 |
|
||||
|
||||
나머지 8개(squash&stretch, straight ahead, arcs, secondary action, exaggeration, solid drawing,
|
||||
appeal, staging)는 캐릭터 애니메이션용이며 UI에 억지로 적용하면 유치해진다.
|
||||
**squash & stretch**만 버튼 프레스에 한해 미세하게(scale 0.96~0.98) 유효하다.
|
||||
|
||||
### 4.3 진입 순서 규칙
|
||||
|
||||
```
|
||||
컨테이너 → 구조(헤더/구분선) → 콘텐츠(텍스트/이미지) → 인터랙션 요소(버튼)
|
||||
```
|
||||
|
||||
각 단계 사이 40~80ms 오버랩. **동시에 시작해서 동시에 끝나면 평면적으로 보인다.**
|
||||
|
||||
퇴장은 **정확히 역순**이고 더 빨라야 한다. 실무에서는 퇴장 시 stagger를 없애고 한꺼번에
|
||||
페이드아웃하는 것이 더 깔끔한 경우가 많다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 공간 연속성 — "어디서 와서 어디로 가는가"
|
||||
|
||||
### 5.1 원칙
|
||||
|
||||
사용자의 머릿속에는 **인터페이스의 공간 모델**이 있다. 모션은 그 모델을 유지시키거나 파괴한다.
|
||||
|
||||
- 동일한 객체는 **같은 정체성을 유지**한 채 이동해야 한다 (shared element transition).
|
||||
- 계층 구조에서 **아래로 들어가면** 확대·전진, **위로 나오면** 축소·후퇴.
|
||||
- 형제 관계 이동은 **수평 슬라이드** (탭, 캐러셀, 스텝).
|
||||
- 모달/시트는 **z축 위**에서 온다 (아래에서 올라오거나, 트리거 지점에서 자라남).
|
||||
|
||||
### 5.2 트리거 지점 원칙 (인과의 시각화)
|
||||
|
||||
**요소는 사용자가 클릭한 곳에서 나와야 한다.** 화면 정중앙에서 페이드인하는 팝오버는
|
||||
"어디서 왔는지" 정보를 버리는 것이다.
|
||||
|
||||
```css
|
||||
/* 클릭 지점을 transform-origin으로 삼는 최소 구현 */
|
||||
.popover {
|
||||
transform-origin: var(--origin-x, 50%) var(--origin-y, 0%);
|
||||
scale: 0.95;
|
||||
opacity: 0;
|
||||
transition: scale 180ms var(--ease-out-quart), opacity 180ms linear;
|
||||
}
|
||||
.popover[data-open] {
|
||||
scale: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// 트리거 버튼의 위치를 팝오버의 origin으로 전달
|
||||
function openPopover(trigger, popover) {
|
||||
const t = trigger.getBoundingClientRect();
|
||||
const p = popover.getBoundingClientRect();
|
||||
popover.style.setProperty('--origin-x', `${t.left + t.width / 2 - p.left}px`);
|
||||
popover.style.setProperty('--origin-y', `${t.top + t.height / 2 - p.top}px`);
|
||||
popover.dataset.open = '';
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 시선 유도
|
||||
|
||||
- 사람의 시선은 **움직이는 것**을 먼저 본다. 따라서 **한 시점에 움직이는 것은 하나**여야 한다.
|
||||
- 동시에 여러 곳이 움직이면 시선이 분산되고 아무것도 강조되지 않는다.
|
||||
- 순차 진입(stagger)은 시선을 **경로를 따라 끌고 간다**. 그 경로가 정보 우선순위와 일치해야 한다.
|
||||
- 에러/알림은 **스크롤 이동 + 짧은 강조**로 유도한다. 화면 밖에 있는 것을 흔들어봐야 아무도 못 본다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 나쁜 모션의 징후 — 실패 진단 체크리스트
|
||||
|
||||
에이전트는 자기가 만든 모션에 대해 아래를 **전부** 확인해야 한다. 하나라도 걸리면 고친다.
|
||||
|
||||
### 6.1 즉시 실격 (반드시 수정)
|
||||
|
||||
| 징후 | 왜 나쁜가 | 처방 |
|
||||
|---|---|---|
|
||||
| 페이지 로드 시 콘텐츠가 애니메이션 때문에 **읽히기까지 지연** | 콘텐츠는 즉시 읽혀야 한다. 애니메이션이 게이트가 되면 안 된다 | 첫 화면(above the fold)은 애니메이션 없이 즉시 표시. 리빌은 스크롤 이후에만 |
|
||||
| 스크롤 리빌이 **매번 재생**됨 | 위아래로 스크롤할 때마다 깜빡여 읽기를 방해 | `once: true` / `animation-fill-mode: forwards` / IntersectionObserver 후 unobserve |
|
||||
| 애니메이션 중 **레이아웃 시프트**(CLS) 발생 | 클릭 대상이 움직여 오조작 유발 | transform/opacity만 애니메이션. 공간은 미리 확보 |
|
||||
| **동시에 3개 이상**의 독립적 애니메이션 | 시선 분산, 인지 부하 | 하나로 통합하거나 순차화 |
|
||||
| 500ms 초과 **차단성** 애니메이션 | 사용자가 기다림 | 400ms 이하로 줄이거나 인터럽트 가능하게 |
|
||||
| **인터럽트 불가** — 애니메이션 중 클릭 무시 | 통제권 상실 | 애니메이션 중에도 입력 수용, 새 목표로 재타겟 |
|
||||
| `prefers-reduced-motion` 미대응 | 접근성 위반, 실제 신체 반응 유발 | 05번 문서 참조 |
|
||||
| 무한 반복되는 **큰 면적** 움직임 | 전정기관 자극 + 배터리 소모 | 정지 가능하게 하거나 제거 |
|
||||
| 스크롤 재킹으로 **네이티브 스크롤 파괴** | 키보드/스크린리더/스크롤바 붕괴 | 03번 문서 Lenis 섹션 참조 |
|
||||
|
||||
### 6.2 냄새 (재검토 필요)
|
||||
|
||||
| 징후 | 진단 질문 |
|
||||
|---|---|
|
||||
| 이징이 전부 `ease` 또는 `linear` | 진입/퇴장/이동을 구분했는가? |
|
||||
| 모든 애니메이션이 같은 duration | 크기·거리·빈도가 다른데 왜 같은가? |
|
||||
| 진입과 퇴장의 duration이 같음 | 퇴장을 0.6~0.75배로 줄였는가? |
|
||||
| `will-change`가 10개 이상 요소에 상시 적용 | GPU 메모리 낭비. 필요할 때만 켜고 끄는가? |
|
||||
| 로딩 스켈레톤이 2초 이상 유지 | 스켈레톤은 지연을 가리는 반창고. 실제 지연을 줄였는가? |
|
||||
| 애니메이션 후에도 포커스가 원래 자리 | 포커스가 시각적 흐름을 따라가는가? |
|
||||
| 텍스트가 글자 단위로 쪼개짐 | 스크린리더가 "ㅇ-ㅏ-ㄴ-ㄴ-ㅕ-ㅇ"으로 읽는다. aria 처리했는가? |
|
||||
| hover에만 존재하는 정보 | 터치 기기와 키보드에서는? |
|
||||
|
||||
### 6.3 "애니메이션을 넣지 말아야 할" 상황
|
||||
|
||||
- **고빈도 반복 작업 흐름**: 데이터 입력, 표 편집, 코드 에디터. 여기서 애니메이션은 순수 비용이다.
|
||||
- **에러 복구 경로**: 사용자가 이미 짜증난 상태. 지연을 추가하지 않는다.
|
||||
- **결과가 이미 예측되는 전환**: 탭 전환처럼 "당연히 이렇게 될 것"인 곳에 화려한 모션은 낭비.
|
||||
- **오프라인/저사양 환경**: 프레임 드랍된 애니메이션은 없느니만 못하다.
|
||||
- **정보 밀도가 높은 대시보드**: 숫자가 계속 바뀌는 곳에서 카운트업 애니메이션은 읽기를 방해한다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 결정 트리 — 에이전트용 실행 규칙
|
||||
|
||||
```
|
||||
[1] 이 모션의 역할이 1~4(인과/연속성/피드백/주의) 중 하나인가?
|
||||
NO → 만들지 않는다. (단 브랜드 표현이 명시 요구사항이면 5번으로)
|
||||
YES ↓
|
||||
|
||||
[2] 사용자의 직접 조작(드래그/제스처)에 물려 있거나, 목표가 도중에 바뀔 수 있는가?
|
||||
YES → spring. bounce는 0~0.25. visualDuration은 아래 [4]의 값.
|
||||
NO ↓
|
||||
|
||||
[3] 스크롤 진행률에 직접 물려 있는가?
|
||||
YES → linear / ease "none". duration 개념 없음. CSS scroll-driven 우선(02번), 불가하면 ScrollTrigger scrub(03번).
|
||||
NO ↓
|
||||
|
||||
[4] 방향은?
|
||||
화면 안으로 들어옴 → ease-out 계열 (--ease-out-quart)
|
||||
화면 밖으로 나감 → ease-in 계열 (--ease-in-quart), duration은 진입의 0.65배
|
||||
화면 안에서 이동 → ease-in-out 계열 (--ease-in-out-quart)
|
||||
|
||||
[5] 지속시간은 3.2 표에서 카테고리를 골라 가져온다.
|
||||
거리가 표준의 2배 이상이면 ×1.4, 표의 상한을 넘지 않는다.
|
||||
|
||||
[6] 여러 요소인가?
|
||||
YES → 4.1 stagger 표. 총 800ms 상한 검증.
|
||||
NO ↓
|
||||
|
||||
[7] 성능 검증: transform/opacity/filter/clip-path 만 애니메이션하는가?
|
||||
NO → 05번 문서의 대체 기법 적용.
|
||||
|
||||
[8] prefers-reduced-motion 대응이 있는가?
|
||||
NO → 05번 문서 4장의 3단계 대응 적용. (전부 끄기는 오답)
|
||||
|
||||
[9] 구현 스택 선택: CSS로 되나?
|
||||
YES → 02번 문서. (기본값. 라이브러리는 필요할 때만)
|
||||
NO → 03번 문서의 선택 기준표.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 복사해서 바로 쓰는 모션 토큰 세트
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* ── Duration ─────────────────────────────────────────── */
|
||||
--dur-instant: 80ms; /* 프레스, 체크, 리플 시작 */
|
||||
--dur-fast: 150ms; /* 호버, 컬러, 아이콘 상태 */
|
||||
--dur-quick: 220ms; /* 툴팁, 드롭다운, 토스트 진입 */
|
||||
--dur-base: 300ms; /* 모달, 시트, 아코디언 진입 */
|
||||
--dur-slow: 420ms; /* 페이지 전환, shared element */
|
||||
--dur-reveal: 600ms; /* 스크롤 리빌 (1회성) */
|
||||
--dur-ambient: 12s; /* 배경 루프 */
|
||||
|
||||
/* 퇴장은 진입의 0.65배 */
|
||||
--dur-quick-out: 145ms;
|
||||
--dur-base-out: 195ms;
|
||||
--dur-slow-out: 275ms;
|
||||
|
||||
/* ── Easing ───────────────────────────────────────────── */
|
||||
--ease-in: cubic-bezier(0.5, 0, 0.75, 0);
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-in-out: cubic-bezier(0.76, 0, 0.24, 1);
|
||||
--ease-back-out: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
--ease-linear: linear;
|
||||
|
||||
/* Material 3 호환 별칭 */
|
||||
--ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
|
||||
--ease-emphasized-decelerate: cubic-bezier(0.05, 0.7, 0.1, 1);
|
||||
--ease-emphasized-accelerate: cubic-bezier(0.3, 0, 0.8, 0.15);
|
||||
|
||||
/* ── Stagger ──────────────────────────────────────────── */
|
||||
--stagger-tight: 25ms;
|
||||
--stagger-base: 40ms;
|
||||
--stagger-loose: 60ms;
|
||||
|
||||
/* ── Distance ─────────────────────────────────────────── */
|
||||
--shift-xs: 4px; /* 미세 피드백 */
|
||||
--shift-sm: 8px; /* 툴팁, 드롭다운 */
|
||||
--shift-md: 16px; /* 카드 리빌 */
|
||||
--shift-lg: 32px; /* 섹션 리빌 */
|
||||
}
|
||||
|
||||
/* reduced-motion에서의 재정의 — "끄기"가 아니라 "줄이기" */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
--dur-instant: 1ms;
|
||||
--dur-fast: 1ms;
|
||||
--dur-quick: 100ms; /* 페이드는 남긴다 */
|
||||
--dur-base: 120ms;
|
||||
--dur-slow: 120ms;
|
||||
--dur-reveal: 120ms;
|
||||
--dur-ambient: 0s;
|
||||
|
||||
--dur-quick-out: 80ms;
|
||||
--dur-base-out: 80ms;
|
||||
--dur-slow-out: 80ms;
|
||||
|
||||
--stagger-tight: 0ms;
|
||||
--stagger-base: 0ms;
|
||||
--stagger-loose: 0ms;
|
||||
|
||||
--shift-xs: 0px;
|
||||
--shift-sm: 0px;
|
||||
--shift-md: 0px;
|
||||
--shift-lg: 0px; /* 이동 거리를 0으로 → 페이드만 남음 */
|
||||
|
||||
--ease-out: ease-out;
|
||||
--ease-in: ease-out;
|
||||
--ease-in-out: ease-out;
|
||||
--ease-back-out: ease-out; /* 오버슛 제거 */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 이 토큰 세트의 핵심 설계: **`prefers-reduced-motion`에서 거리 토큰을 0으로 만들면
|
||||
> 모든 슬라이드 애니메이션이 자동으로 크로스페이드가 된다.** 컴포넌트 코드를 한 줄도
|
||||
> 고치지 않고 전역에서 모션 강도를 낮출 수 있고, `animationend` 이벤트 의존 코드도 깨지지 않는다.
|
||||
1530
research/motion/02-css-native.md
Normal file
1530
research/motion/02-css-native.md
Normal file
File diff suppressed because it is too large
Load diff
1797
research/motion/03-js-stacks.md
Normal file
1797
research/motion/03-js-stacks.md
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue