動態字體排版 (Kinetic Typography)
製作影片時,讓文字本身成為畫面。一個短語出現,填滿畫面,被重擊後被下一個短語替換,並配合節奏。應該感覺大膽、快速且富有音樂性,就像會動的海報。
選項
| 選項 | 預設值 | 備註 |
|---|---|---|
| 格式格式 | 垂直 1080×1920 | 或橫向 1920×1080、正方形 1080×1080 |
| 時間 | 10–20 秒 | 與旁白或歌曲段落匹配 |
| 風格 | 強勁 | calm(0.4 秒的滑入淡出,較少翻轉)、punchy(預設)、chaotic(全大寫、撞擊、抖動、每隔一個短語就翻轉) |
| 節奏 | 固定節拍 | 或旁白單詞時間、或音樂節拍 |
簡報 (規劃前回答)
- 觀眾應該記住的一句話是什麼?
- 將腳本拆分為每個 1–4 個詞的短語;為每個短語標記一個英雄詞。
- 能量高峰在哪裡(獲得顏色翻轉的短語)?
- 結尾鎖定畫面是什麼(標題、呼籲行動、帳號名稱)?
閱讀表 (與節拍清單一起撰寫)
對於每個節拍,寫下觀眾必須理解的一件事(「機器人不能飛」、「快取未命中」)以及它保持可讀的時間。一次只閱讀一個。文字地板:一個標籤至少停留 0.8 秒,一個句子一旦完全顯示在螢幕上,每個詞大約 0.3 秒。如果文字不符合時間要求,則減少詞彙而不是加快速度。
運動紀律
- 運動是有因果關係的:每個動作在螢幕上都有原因(點擊、掉落、資料包到達),下一個動作從中產生。
- 為整個影片選擇一個主要運動方向並保持不變。使用其他方向來表達意義:向上表示成功或結論,縮小表示「這是整體情況」。
- 一個場景的退出為下一個場景的進入做好準備:相同的軸、相同的方向、相似的速度。在變化之間攜帶一個物件,而不是交叉淡化。
- 在圖表、圖表或文字上不要有無聊的填充動畫(浮動、呼吸、搖擺)。如果沒有東西應該移動,就保持靜止;一次刻意鏡頭移動就足夠了。
- 緩動:順暢的 ease-out 用於進入(
power2.out到power4.out),較快的 ease-in 用於退出(約為進入時間的 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) | 使用時機 |
|---|---|---|
| 撞擊 | from {scale: 2.6, opacity: 0},expo.out,0.22 秒,加上 6 px 螢幕抖動 0.1 秒 | 英雄詞 |
| 滑動堆疊 | 行數 from {yPercent: 110} 在 overflow:hidden 遮罩內,間隔 0.06 秒,power4.out 0.35 秒 | 列表、兩行短語 |
| 拉伸 | scaleY: 1.6 → 1,transformOrigin: bottom,back.out(3) 0.3 秒 | 強調、動詞 |
| 字母級聯 | 字母 from {y: -120, rotation: -20, opacity: 0},間隔 0.025 秒,back.out(2) | 名字、標題 |
| 翻轉替換 | 舊詞 rotationX: 90 出去 (0.12 秒),新詞 rotationX: -90 → 0 進來 (0.18 秒) | X 變成 Y |
| 分離 | 單詞兩半滑開 x: ±140 以在之間顯示一個較小的詞 | 對比 |
| 退出 | 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%,英雄擊中時放大 (scale 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>
// [時間, 行數, 動作名稱, 英雄詞索引, 是否翻轉背景?] -- 編輯此列表;時間以秒為單位
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 畫面的底部 670 px 或頂部 270 px 內(平台 UI 會遮蓋它們)。
- 隨機或每次渲染不同的變化。每個值必須來自時間軸。
失敗模式
| 看起來像 | 原因 | 修復方式 |
|---|---|---|
| 簡報 | 淡入淡出、長時間停留、每次動作相同 | 從詞彙中變化動作;進入時間低於 0.35 秒;退出時間 0.15 秒 |
| 字幕,非設計 | 文字太小,居中在空白空間 | 將文字大小調整到畫面寬度;使用壓縮字體;250+ px |
| 混亂、難以閱讀 | 一次太多文字或動作 | 一次只顯示一個短語;一個英雄詞;落地後保持 0.35 秒 |
| 不跟拍 | 短語時間是猜測的 | 使用轉錄或節拍時間;讓擊中提前 2–3 幀 |
| 文字被遮罩裁剪 | 行高對於字體來說太緊 | 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 評分:鉤子 (第一秒)、手機寬度可讀性、運動品質、構圖、準確性,以及「靜音測試」(觀眾能否在沒有聲音的情況下用一句話總結它?)。只修復得分 7 或以下的項目,重新拍攝快照,並重複直到所有項目得分 8 或以上。如果可以,讓一個獨立的唯讀評估來評分,這樣你不是在評判自己的工作。
| 不要 | 而應該 |
|---|---|
| 在兩個繁忙的版面之間交叉淡入淡出 | 先退出舊內容,然後引入新內容,或攜帶一個物件橫跨 |
| 讓多個東西同時移動 | 錯開時間,一次一個焦點點 |