VYBE 모션 디자인
움직임이 곧 메시지입니다. 모든 움직임은 주제에 대해 무언가를 말해야 합니다 — 무게, 속도, 자신감, 경이로움 — 그렇지 않다면 존재해서는 안 됩니다.
모든 레시피는 HyperFrames용입니다: 구성당 하나의 일시정지된 GSAP 타임라인, 결정론적이며 seek 가능합니다. 입장에는 fromTo를, 애니메이션에는 transforms + opacity + clipPath + filter를 사용하며, 레이아웃 속성은 절대 사용하지 않습니다.
참조 파일 (필요한 것을 로드하세요)
| 파일 | 용도 |
|---|---|
references/animation-principles.md | UI/타이포그래픽/그래픽에 적용된 12가지 원칙, 이징 어휘, 타이밍 테이블 |
references/transitions.md | 14가지 전환 레시피(GSAP 코드 포함) 및 각각을 사용해야 하는 경우 |
references/camera-movements.md | 돌리, 트럭, 크레인, 오빗, 줌-스루, 휩, 랙 포커스, 패럴랙스 — 2D 및 3D |
references/visual-effects.md | 글로우, 블룸, 라이트 릭, 필름 그레인, 크로매틱 스플릿, 모션 블러, 마스크, 모프 |
열 가지 규칙
- 애니메이션하지 말고 안무하세요. 누가 먼저 움직이고, 누가 응답하며, 누가 멈출지 계획하세요. 장면은 문장입니다: 주어 → 동사 → 목적어.
- 이징은 개성입니다.
expo.out= 프리미엄/자신감;back.out= 장난기;power2.inOut= 기계적인 카메라;sine.inOut= 숨쉬는 듯한/앰비언트;none= 일정한 드리프트. 영화당 2~3개의 이징을 선택하고 일관성을 유지하세요. - 빠르게 들어와서 천천히 안착하세요. 대부분의 입장: 0.5~0.9초, 움직임의 80%가 처음 30%의 시간에 발생합니다.
- 퇴장은 입장보다 빠릅니다(약 60%) 그리고
.in이징을 사용합니다. - 스태거는 계층을 드러냅니다. 글자: 0.04
0.08초, 단어: 0.10.18초, 목록 항목: 0.3~0.6초. 모든 것을 동일하게 스태거하지 마십시오. - 액션을 겹치세요. 다음 요소는 이전 요소가 끝나기 전에 시작됩니다 (position 파라미터
"<0.2"/ 절대 시간). 순차적 = 로봇 같습니다. - 읽을 시간을 주세요. 단어당 약 0.3초 + 0.5초; 마지막 줄은 2초 이상.
- 항상 살아있는 움직임을 유지하세요. 멈춤 중에는 느린 푸시(
scale 1→1.04), 드리프트 또는 라이트 스위프가 프레임을 죽지 않게 합니다. 단, 레이어당 하나의 앰비언트 모션만 허용합니다. - 전환을 다양하게 하세요. 영화당 최대 한 번의 검은색 페이드-스루. 모션 컷, 매치 컷, 마스크 와이프, 줌-스루, 공유 요소를 선호하세요.
- 비트를 존중하세요. 음악이 있다면, 모든 주요 변화는 비트에 맞춰 발생해야 합니다(
60/bpm); 히트는 다운비트 에 맞고, 예측 동작은 그보다 먼저 시작됩니다.
Seek-safe GSAP 패턴
const tl = gsap.timeline({ paused: true });
// 입장: 블러에서 선명하게 상승 (프리미엄)
tl.fromTo("#s1-title", { opacity: 0, y: 40, filter: "blur(12px)" },
{ opacity: 1, y: 0, filter: "blur(0px)", duration: 0.9, ease: "expo.out" }, 0.2);
// 마스크 와이프 리빌 (에디토리얼)
tl.fromTo("#s1-line", { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.8, ease: "expo.inOut" }, 0.5);
// 글리프별 퍼짐 (letterSpacing 대신 — lint가 거부함)
gsap.utils.toArray("#s1-word .ch").forEach((c, i, all) => {
const off = (i - (all.length - 1) / 2) * -30;
tl.fromTo(c, { x: off, opacity: 0 }, { x: 0, opacity: 1, duration: 1, ease: "expo.out" }, 1 + Math.abs(i - (all.length - 1) / 2) * 0.05);
});
// 카운트 업 (seek 가능: onUpdate는 모든 seek에서 실행됨)
const n = { v: 0 }, el = document.getElementById("s1-num");
tl.fromTo(n, { v: 0 }, { v: 97, duration: 1.4, ease: "expo.out", onUpdate: () => (el.textContent = Math.round(n.v)) }, 1.2);
// 앰비언트 홀드
tl.fromTo("#s1-stack", { scale: 1 }, { scale: 1.04, duration: 3, ease: "sine.inOut" }, 0);
window.__timelines["s1"] = tl;
텍스트 분할: 단어/글자를 <span class="w|ch" style="display:inline-block">로 감쌉니다 (transforms는 인라인 요소에서 무효). 공백은 스팬 밖에 유지하세요.
인계 전 확인 사항
- 연속된 두 장면이 동일한 입장 모션을 공유하지 않아야 합니다.
- 동일한 요소에서 겹치는 시간에 속성이 두 번 트윈되지 않아야 합니다.
npx hyperframes check명령어로 Motion 섹션이 깨끗한지 확인하세요. 안무 감사를 위해 HyperFrames의/hyperframes-animation스킬이 설치된 경우 함께 제공되는animation-map.mjs스크립트를 실행하세요.