운동감 타이포그래피 (Kinetic Typography)
단어 자체가 그림이 되는 비디오를 만듭니다. 한 구문이 나타나 프레임을 채우고, 강하게 부딪힌 후 다음 구문으로 교체되며 리듬을 탑니다. 마치 움직이는 포스터처럼 대담하고 빠르며 음악적이어야 합니다.
옵션
| 옵션 | 기본값 | 참고 |
|---|---|---|
| format | 세로 1080×1920 | 또는 가로 1920×1080, 정사각형 1080×1080 |
| duration | 10–20 초 | 음성 또는 노래 구간에 맞춤 |
| tone | punchy | calm (0.4초 슬라이드 및 페이드, 적은 뒤집기), punchy (기본값), chaotic (모두 대문자, 부딪힘, 흔들림, 매 두 번째 구문마다 뒤집기) |
| rhythm | 고정 템포 | 또는 음성 단어 시간, 또는 음악 비트 |
브리핑 (계획 전에 답변)
- 시청자가 기억해야 할 한 문장은 무엇인가요?
- 스크립트를 1–4개 단어의 구문으로 나누고, 각 구문마다 하나의 히어로 단어를 표시하세요.
- 에너지가 최고조에 달하는 지점(색상 뒤집기가 발생하는 구문)은 어디인가요?
- 마무리 잠금 장면(타이틀, 행동 유도 문구, 핸들)은 무엇인가요?
읽기 시트 (비트 목록과 함께 작성)
각 비트마다 시청자가 반드시 이해해야 할 한 가지("로봇은 날 수 없다", "캐시가 누락되었다")와 그것이 읽을 수 있는 상태로 유지되는 시간을 작성하세요. 한 번에 하나의 읽기입니다. 텍스트 바닥: 레이블은 최소 0.8초, 문장은 화면에 완전히 나타난 후 단어당 약 0.3초입니다. 텍스트가 시간에 맞지 않으면 속도를 높이지 말고 단어를 줄이세요.
모션 규율
- 모션은 인과적입니다. 모든 움직임에는 화면에 이유(클릭, 낙하, 패킷 도착)가 있으며, 다음 움직임은 그로부터 이어집니다.
- 전체 비디오의 주요 이동 방향 하나를 선택하고 유지하세요. 다른 방향은 의미에 사용하세요: 위쪽은 성공 또는 결론, 축소는 "여기 전체 그림이 있습니다".
- 장면의 종료는 다음 항목을 설정합니다: 동일한 축, 동일한 방향, 유사한 속도. 크로스페이드 대신 하나의 오브젝트를 변경점을 가로질러 가져오세요.
- 다이어그램, 차트 또는 텍스트에 유휴 필러 모션(떠다니기, 숨쉬기, 흔들리기)은 없습니다. 아무것도 움직이지 않아야 하면 멈추고, 의도적인 카메라 드리프트 하나면 충분합니다.
- 이징: 항목은 부드러운 이즈 아웃(
power2.out~power4.out), 종료는 더 빠른 이즈 인(항목 시간의 약 75%). 항목은 최대 0.8초. 그룹의 총 지연 시간은 최대 0.5초. 오버슈트(back.out)는 작은 팝에만 사용; 탄성이나 바운스는 사용하지 마세요. - 중요할 때는 한 번에 하나의 것만 움직입니다. 눈은 하나만 따라갈 수 있습니다.
- 결정적 순간 직전에 0.3–0.75초의 정적 비트를 추가하세요.
스타일 규칙 (항상)
이것이 이 스타일을 만듭니다. 색상이나 단어와 관계없이 유지하세요.
- 텍스트는 프레임 너비의 50–80%를 채웁니다. 작은 텍스트는 캡션으로 읽히고 시각적 요소로 읽히지 않기 때문입니다.
- 한 번에 한 가지 아이디어: 1–4개 단어, 최대 3줄. 다른 모든 것은 사라졌습니다.
- 모든 단어는 움직임(슬램, 슬라이드, 스트레치, 마스크 공개, 플립)과 함께 도착합니다. 아무것도 단순히 페이드 인하지 않습니다.
- 변화는 리듬에 맞춰 발생합니다: 0.25–0.7초마다 새 단어 또는 움직임, 키워드는 비트 또는 강세 음절에.
- 대비는 극단적입니다: 거의 검은색 위에 거의 흰색 또는 그 반대, 핵심 단어에만 하나의 악센트 색상.
- 각 구문에는 하나의 히어로 단어가 있습니다: 가장 크거나, 악센트 색상이거나, 나머지와 다르게 움직입니다.
기본값 (자유롭게 변경)
글꼴, 색상, 단어, 타이밍 및 배경. 사용자가 브랜드 글꼴이나 색상을 제공하면 그것이 우선합니다. 우선 순위: 사용자의 브랜드 또는 사양, 명시적 요청, 그 다음 이 기본값.
- 배경
#111111, 텍스트#f4f1ea, 악센트#ff4d2e. 에너지를 위해 구문마다 밝음 위에 어두움 또는 어두움 위에 밝음으로 뒤집습니다. - 디스플레이 글꼴: 무겁고 좁은 얼굴(Anton, Bebas Neue 또는 Archivo Black), 히어로 단어는 모두 대문자. 지원 글꼴: 작은 연결 단어를 위한 무거운 산세리프(Inter 800).
- 크기: 9:16에서 히어로 단어 180–320 px; 작은 단어 60–90 px.
설정 (한 번)
node -v # 22+ 필요
ffmpeg -version
npx hyperframes init my-video --non-interactive --resolution portrait && cd my-video # 16:9는 landscape
기존 빈 폴더 안에서는 npx hyperframes init . --non-interactive --resolution portrait를 사용한 다음, 생성된 index.html을 아래 템플릿으로 교체하세요.
빌드 전 결정
코드를 작성하기 전에 이것들을 한 줄로 사용자에게 다시 알리세요.
- 종횡비: 9:16 (1080×1920) 또는 16:9 (1920×1080). 기본값은 9:16입니다.
- 단어 및 타이밍: 스크립트를 1–4개 단어의 구문으로 나눕니다. 음성이 있는 경우, 단어 타임스탬프(
npx hyperframes transcribe voice.wav)에서 시간을 가져옵니다. 없는 경우, 각 구문에 0.6–1.4초를 주고 히어로 단어에는 더 긴 시간을 줍니다. - 리듬 소스: 음성(강세 음절에 맞춤), 음악(비트에 맞춤;
npx hyperframes beats music.mp3), 또는 고정 템포(예: 120 BPM = 0.5초마다 비트). - 구문당 히어로 단어 및 어떤 움직임을 받을지.
| 필요 항목 | HyperFrames + GSAP (기본값) | Remotion |
|---|---|---|
| 단어 및 문자 움직임, 마스크, 스트레치 | 예 | 예 |
| 음성 또는 비트 데이터 기반 타이밍 | 예 (레이블) | 예 (프레임) |
| Node와 FFmpeg만으로 실행 | 예 | 예 |
| 라이선스 | 무료 | 최대 3명까지 무료, 그 이상은 유료 |
움직임 어휘
| 움직임 | 방법 (GSAP) | 용도 |
|---|---|---|
| Slam | from {scale: 2.6, opacity: 0}, expo.out, 0.22 초, 6px 화면 흔들림 0.1 초 추가 | 히어로 단어 |
| Slide stack | 줄 from {yPercent: 110} 내부 overflow:hidden 마스크, 지연 0.06 초, power4.out 0.35 초 | 목록, 두 줄 구문 |
| Stretch | scaleY: 1.6 → 1, transformOrigin: bottom, back.out(3) 0.3 초 | 강조, 동사 |
| Letter cascade | 문자 from {y: -120, rotation: -20, opacity: 0}, 지연 0.025 초, back.out(2) | 이름, 제목 |
| Flip swap | 이전 단어 rotationX: 90 out (0.12 초), 새 단어 rotationX: -90 → 0 in (0.18 초) | X가 Y가 됨 |
| Split | 단어 반쪽이 x: ±140로 미끄러져 작은 단어를 그 사이에 드러냄 | 대조 |
| Exit | yPercent: -110 또는 scale: 0.7, opacity: 0, 0.15 초, power2.in | 모든 구문이 떠남 |
종료는 빠르고(0.12–0.18초), 입장은 생각보다 더 빠릅니다(0.18–0.35초). 각 구문이 도착한 후 최소 0.35초 동안 유지하여 읽을 수 있게 하세요.
시선을 사로잡는 방법
- 프레임을 채우세요. 단어를 글꼴 크기가 아닌 프레임 너비에 맞게 크기를 조정하세요. 250+ px의 좁은 글꼴이 룩입니다.
- 색상 뒤집기: 배경이 2–3개의 핵심 구문에서 어두움과 밝음(또는 악센트) 사이를 비트의 하드 컷으로 전환합니다.
- 질감과 깊이: 미세한 그레인 오버레이, 히어로 단어 뒤의 부드러운 그림자 또는 오프셋 복제본(악센트 색상, 오프셋 10–14 px), 그리고 배경에 가끔 거대하고 흐릿한 단어.
- 카메라 에너지: 전체 스테이지에 느린 2–4% 스케일 드리프트, 히어로 히트에서 펀치인(스케일 1.06, 0.1 초 동안), 슬램에서 6–10 px 흔들림.
- 단어와 함께하는 도형: 그려지는 밑줄 막대, 단어 뒤에 나타나는 원, 화살표. 악센트 색상, 평면.
- 잠금 장면으로 끝내기: 완전히 구성된 제목 또는 행동 유도 문구를 1–1.5초 유지, 시작 프레임과 일치시켜 루프되도록 합니다.
- 품질 게이트: 스냅샷에 평범한 배경에 작은 중앙 정렬 텍스트가 보이면 완료된 것이 아닙니다.
빌드: 템플릿
<!doctype html><html><head><meta charset="UTF-8">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Anton&family=Inter:wght@800&display=swap">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<style>
html,body{margin:0;width:1080px;height:1920px;overflow:hidden;background:#111}
#root{width:100%;height:100%;position:relative;overflow:hidden}
#stage{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}
.phrase{position:absolute;left:60px;right:120px;top:270px;bottom:670px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:0}
.line{overflow:hidden;line-height:.92;padding:0 8px}
.w{display:inline-block;font-family:Anton,sans-serif;text-transform:uppercase;color:#f4f1ea;white-space:nowrap}
.hero{color:#ff4d2e}
.flip .w{color:#fff}.flip .hero{color:#111}
.small{font-family:Inter,system-ui,sans-serif;font-weight:800;text-transform:none}
#bg{position:absolute;inset:0;background:#111}
</style></head>
<body><div id="root" data-composition-id="main" data-start="0" data-duration="8" data-width="1080" data-height="1920">
<div id="bg"></div><div id="stage"></div></div>
<script>
// [time, lines, moveName, heroWordIndex, flipBackground?] -- 이 목록을 편집하세요; 시간(초)
const PHRASES = [
[0.2, ["WORDS"], "slam", 0],
[1.0, ["can", "MOVE"], "stack", 1],
[2.0, ["LIKE", "MUSIC"], "stretch", 1, true],
[3.2, ["one", "idea"], "stack", 1],
[4.2, ["AT A", "TIME"], "slam", 1],
[5.4, ["KINETIC", "TYPE"], "cascade", 1],
];
const END = 8, W = 1080 - 180;
// 웹 폰트가 로드된 후에만 텍스트를 측정한 다음 타임라인을 빌드하고 등록합니다.
document.fonts.ready.then(() => {
const tl = gsap.timeline({ paused: true });
const stage = document.getElementById("stage");
PHRASES.forEach(([t, lines, move, hero, flip], i) => {
const p = document.createElement("div"); p.className = "phrase" + (flip ? " flip" : ""); stage.appendChild(p);
const words = lines.map((txt, k) => {
const line = document.createElement("div"); line.className = "line";
const w = document.createElement("span"); w.className = "w" + (k === hero ? " hero" : "") + (txt === txt.toLowerCase() && k !== hero ? " small" : "");
w.textContent = txt; line.appendChild(w); p.appendChild(line);
w.style.fontSize = "100px"; const fit = Math.min(420, (100 * W) / w.getBoundingClientRect().width); // 너비 채우기
w.style.fontSize = (w.classList.contains("small") ? Math.min(fit, 110) : fit) + "px";
return w;
});
gsap.set(p, { autoAlpha: 0 });
tl.set(p, { autoAlpha: 1 }, t);
if (move === "slam") tl.from(words, { scale: 2.6, opacity: 0, duration: 0.22, ease: "expo.out", stagger: 0.08 }, t)
.fromTo("#stage", { x: -10 }, { x: 10, duration: 0.04, yoyo: true, repeat: 3, ease: "none" }, t + 0.18).set("#stage", { x: 0 }, t + 0.34);
if (move === "stack") tl.from(words, { yPercent: 110, duration: 0.35, ease: "power4.out", stagger: 0.07 }, t);
if (move === "stretch") tl.from(words, { scaleY: 1.7, opacity: 0, transformOrigin: "50% 100%", duration: 0.3, ease: "back.out(3)", stagger: 0.08 }, t);
if (move === "cascade") words.forEach((w, k) => {
const letters = Array.from(w.textContent); w.textContent = "";
letters.forEach((c) => { const l = document.createElement("span"); l.style.display = "inline-block"; l.textContent = c; w.appendChild(l); });
tl.from(w.children, { y: -140, rotation: -20, opacity: 0, duration: 0.4, ease: "back.out(2)", stagger: 0.03 }, t + k * 0.15); });
const next = PHRASES[i + 1] ? PHRASES[i + 1][0] : null;
if (flip) tl.set("#bg", { background: "#ff4d2e" }, t).set("#bg", { background: "#111" }, next || END); // 키 구문에서 색상 뒤집기
if (next) tl.to(words, { yPercent: -110, opacity: 0, duration: 0.15, ease: "power2.in", stagger: 0.03 }, next - 0.17)
.set(p, { autoAlpha: 0 }, next);
});
tl.fromTo("#root", { scale: 1 }, { scale: 1.04, duration: END, ease: "none" }, 0); // 느린 드리프트
tl.set({}, {}, END);
window.__timelines["main"] = tl;
});
</script></body></html>
PHRASES를 편집하여 단어와 시간을 변경하세요. 움직임 어휘에서 if (move === ...) 분기를 추가하여 움직임을 추가하세요. 음성이 있는 경우, 루트에 <audio> 요소를 넣고 구문 시간을 트랜스크립트에서 가져오세요.
사용자에게 노출할 매개변수
aspect · phrases (시간, 줄, 움직임, 히어로) · fontDisplay · fontSupport · colors (bg, text, accent) · tempo or audio · flipPhrases (배경이 뒤집히는 위치) · endLockup.
피해야 할 것
- 페이드 인과 느린 드리프트를 주요 모션으로 사용하는 것. 슬라이드쇼로 읽힙니다.
- 한 번에 4개 이상의 단어 또는 3줄을 화면에 표시하는 것.
- 여러 개의 악센트 색상. 하나의 악센트, 히어로 단어에만 사용.
- 히어로 단어에 얇거나 가벼운 글꼴 두께 또는 스크립트 글꼴.
- 9:16 프레임의 하단 670px 또는 상단 270px 내의 텍스트 (플랫폼 UI가 덮음).
- 무작위 또는 렌더링별 변형. 모든 값은 타임라인에서 가져와야 합니다.
실패 모드
| 모양 | 원인 | 수정 |
|---|---|---|
| 슬라이드쇼처럼 보임 | 페이드, 긴 유지, 매번 같은 움직임 | 어휘에서 움직임을 다양화; 입장 0.35초 미만; 종료 0.15초 |
| 디자인이 아닌 캡션처럼 보임 | 단어가 너무 작고 빈 공간에 중앙 정렬됨 | 단어를 프레임 너비에 맞게 크기 조정; 좁은 글꼴; 250+ px |
| 지저분하고 읽을 수 없음 | 한 번에 너무 많은 단어 또는 움직임 | 화면에 하나의 구문; 하나의 히어로 단어; 도착 후 0.35초 유지 |
| 박자에서 벗어남 | 구문 시간을 추측함 | 트랜스크립트 또는 비트 시간 사용; 히트를 2–3프레임 일찍 착지 |
| 마스크에 의해 단어 잘림 | 글꼴에 비해 line-height가 너무 좁음 | line-height 0.92–1.0, .line에 패딩 추가; 스냅샷 확인 |
| 렌더링에 잘못된 글꼴 | 폰트가 로드되지 않음 | head에 Google Fonts <link>; 스냅샷에 좁은 얼굴이 표시되는지 확인 |
최종 렌더링 전에 확인
npx hyperframes check
npx hyperframes snapshot --at <구문이 도착한 후 0.3초 시점 하나당 한 번, 그리고 종료 중간에 한 번>
npx hyperframes render -o renders/<name>.mp4
ffprobe -v error -show_entries stream=width,height,r_frame_rate:format=duration -of csv=p=0 renders/<name>.mp4
모든 스냅샷과 snapshots/contact-sheet.jpg를 보고 다음을 확인하세요:
- 모든 구문이 너비의 50–80%를 채우고 휴대폰 크기에서 읽을 수 있음
- 구문당 하나의 히어로 단어, 악센트 색상 또는 다른 움직임
- 어떤 프레임에서도 두 구문이 겹치지 않음
- 모든 텍스트가 종횡비에 대한 안전 영역 내에 있음
- 마무리 잠금 장면이 최소 1초 유지되고 시작과 일치함
- 크기와 지속 시간이 요청과 일치함
최종 렌더링 전 자기 비평
스냅샷 시트에 1–10점으로 다음을 평가하세요: 후크(첫 1초), 휴대폰 너비에서의 가독성, 모션 품질, 구성, 정확성, "음소거 테스트"(시청자가 소리 없이 한 문장으로 요약할 수 있는가?). 7점 이하인 항목만 수정하고, 다시 스냅샷을 찍고 모든 항목이 8점 이상이 될 때까지 반복하세요. 가능하다면 별도의 읽기 전용 패스가 점수를 매기도록 하여 자신의 작업을 평가하지 않도록 하세요.
| 하지 말 것 | 대신 |
|---|---|
| 두 개의 혼잡한 레이아웃 사이를 크로스페이드 | 기존 콘텐츠를 종료한 다음 새 콘텐츠를 가져오거나, 하나의 오브젝트를 가로질러 가져오기 |
| 여러 개를 동시에 움직이기 | 지연 시간을 두고, 하나의 초점 |