動態字體排版 是做什麼的?
文字在動態中,首要任務就是可讀性。動畫字型最常見的失敗不是難看,而是文字在觀眾看完第一個字之前就閃過,或是文字動畫太多,導致眼睛追逐動態而無法閱讀內容。這是工藝層面。它決定每一行停留多久、如何進出、以及什麼可以移動——並將具體的幀數預算交給 remotion-compose,後者負責編寫實際渲染的 React/TypeScript 程式碼。
最高指導原則:動畫是為了服務閱讀,而非裝飾。 動態只有當它能引導視線、建立層級或標記節拍時,才有其存在的價值。以下所有內容都遵循此原則。
以一個完整的範例貫穿全文:一個 16:9 的產品標題卡,內容為 "Ship video in one prompt" (5 個字),以 30fps 呈現,並加上一個動態的下方三分之一處字幕軌道。
工作流程
按順序執行這些步驟。在計算停留預算之前,不要選擇進場動畫——預算會告訴你是否有空間進行動畫。
第 1 步 - 先計算每字閱讀時間預算
文字必須在螢幕上停留足夠長的時間,以便以從容的速度閱讀,再加上其進場和退場消耗的時間。在接觸任何動畫之前,先計算停留預算。
- 螢幕上影片的基準閱讀速度比印刷品慢:假設舒適閱讀速度約為 ~3.5 字/秒 (≈ 200 wpm)。字幕慣例類似——切勿低於每字約 0.3 秒的可讀停留時間。
- 最低螢幕停留時間 = 字數 ÷ 3.5 秒,任何行最低為 0.8 秒 (即使一個字也需要一拍來讓觀眾感知),若包含陌生術語、數字或名稱,則需額外 +0.3 秒稅。
- 進場和退場時間是額外的——不計入閱讀時間,因為文字在過渡期間並非完全可讀。總時段 = 進場 + 停留 + 退場。
執行 type_budget.js (如下) 將一行文字轉換為幀數預算。對於標題卡,它會返回約 43 幀的停留時間;這就是你交給 remotion-compose 作為 Sequence 可讀視窗的數字,進場/退場時間則在此基礎上增加。
第 2 步 - 選擇與剪輯匹配的進場/退場時機,而非預設值
進場和退場是標點符號,而非句子本身。保持它們簡短,讓停留時間發揮主要作用。
- 進場:8-15 幀 (0.25-0.5s) @ 30fps。 要快到感覺乾脆,慢到能被解讀為有意為之。標題卡可以使用較長的端點;快速的字幕軌則使用較短的端點。
- 退場:等於或短於進場。 退場絕不應拖沓——一旦閱讀完畢,文字應立即清除。6-12 幀。
- 讓退場與後續內容匹配。 硬切到下一個場景 → 在剪輯時退出文字 (無淡出,讓剪輯本身完成)。交叉溶解 → 在溶解過程中淡出文字。切勿為硬剪輯設計複雜的退場動畫;這會與剪輯衝突。這是
motion-design-principles(剪輯和緩動) 與此技能交匯之處——從那裡取得緩動曲線,並在此應用於文字。 - 交錯顯示多元素揭示,不要讓它們同步動畫。標題 + 副標題的最佳閱讀體驗是副標題在標題穩定後 4-8 幀進入,以便視線由上而下被引導。
第 3 步 - 每行選擇一種進場技巧,並動畫化一個屬性
同時動畫化的屬性越多,文字在過渡期間就越難閱讀。預設為動畫化單一屬性;只有在有明確意圖時才結合兩個屬性 (例如,不透明度 + 小幅度位移)。
- 淡入 (不透明度 0→1) - 最安全,始終可讀,是字幕軌和正文行的正確預設。
- 上升 / 滑入 (小幅度位移,20-60px,結合淡入) - 增加方向感和生命力;是標題卡的主力。保持距離短小——大幅滑動會讓眼睛追蹤移動而非閱讀。
- 逐字揭示 - 每個字按順序以短暫的交錯時間淡入/上升。用於強調行和歌詞風格的作品,不適用於密集的字幕 (閱讀會變得不連貫)。務必讓整行的揭示在停留時間內完成。
- 縮放 / 彈跳 (彈簧效果從 0.85→1) - 充滿活力,適合單個英雄字詞或數字標註。切勿縮放整個句子;重排會導致無法閱讀。
- 遮罩 / 擦拭揭示 - 高級、有方向性,非常適合單個標題。建置成本較高;保留給最重要的那個標題。
無論選擇哪種方式,動畫化的是屬性;文字本身在動畫過程中絕不重新排版。 動畫化 opacity 和 transform (位移/縮放/裁剪) —— 絕不動畫化 font-size、width 或 letter-spacing 而導致字形重新排版,因為版面驅動的動畫會抖動且無法閱讀。
第 4 步 - 將粗細和大小過渡作為轉換,而非噪點
粗細和大小是字型在動態中表達強調的方式——但它們是停留狀態之間的轉換,而非連續的晃動。
- 粗細過渡: 一個詞可以在穩定下來時從較輕的粗細過渡到較重的粗細以強調重點 (例如,在 8 幀內從 400→700)。使用真正的可變字型粗細軸或字體家族的兩種字重;切勿透過縮放來偽造粗體。
- 大小過渡: 數字或關鍵詞可以一次性放大以佔據層級地位,然後停留。縮放並停留,而非縮放並脈動。文字上的循環脈動讀起來像渲染錯誤,而非強調。
- 這些是重點。每行最多一個粗細/大小強調,放在最重要的那個詞上。如果所有東西都強調,就等於什麼都沒強調。
第 5 步 - 設定動態中的字型層級結構 (順序、大小、粗細、時機)
動態中的層級結構由四個訊號構成;一起使用它們,讓眼睛知道看哪裡以及按什麼順序看。
- 進場順序 - 最先動畫化進來的內容會被最先閱讀。以標題開頭,接著是副標題,最後是行動呼籲。
- 大小對比 - 保持清晰的比例 (標題 ≥ 1.6× 副標題)。動態無法挽救糟糕的靜態層級結構。
- 粗細對比 - 越粗 = 越重要。螢幕上同時最多兩個粗細層級。
- 時機 - 最重要的行獲得最長的停留時間,如果有的話,也獲得最有存在感的進場。最不重要的行 (持續存在的下方三分之一處、浮水印) 在初次進場後就不再動畫化。
使用下方的 motion-type-spec 模板填入每行的預算、技巧和層級角色。填寫完成的規格就是交給 remotion-compose 的交接文件。
第 6 步 - 決定哪裡不該動畫化
高階的做法是克制。在定稿前,從下方 "請勿" 清單中移除任何不必要的動畫。一個經過良好預算的上升標題卡,比每個字都有動畫的標題卡讀起來更好。
品質標準
一個完成的動態字型規格只有在以下所有停留條件都滿足時才能達到 A+:
- 每行都通過閱讀時間預算 - 停留 ≥
max(0.8s, 字數 ÷ 3.5 + 術語稅),由type_budget.js計算,且進場/退場時間在其基礎上增加,絕不從中扣除。 - 每行動畫化一個屬性 (只有明確意圖時才動畫化兩個),且沒有行在動畫過程中重新排版其字形 - 僅限
opacity/transform。 - 進場為 8-15 幀,退場 ≤ 進場,且退場與後續剪輯匹配 (硬切 vs. 溶解),而非使用預設值。
- 層級結構按順序閱讀:進場順序、大小比例 (≥1.6×) 和粗細都指向同一行最重要的內容。
- 每行最多一個粗細/大小強調,且沒有循環或脈動。
- 規格指定了幀數 (進場開始、停留開始、停留結束、退場結束),以便
remotion-compose無需猜測即可編寫 Sequence。
請勿
- 請勿 讓文字停留時間短於閱讀預算——這是動態字型最常見的失敗。如果沒有空間停留,請縮減文字,而非縮減停留時間。
- 請勿 動畫化
font-size、width、letter-spacing或任何會導致字形重新排版的屬性;版面抖動會無法閱讀。請改用transform進行縮放。 - 請勿 每行動畫化超過一或兩個屬性,且切勿對密集字幕進行逐字動畫化——閱讀會變得不連貫。
- 請勿 循環、脈動、反覆彈跳或讓文字持續運動;不停止的運動無法閱讀。
- 請勿 動畫化每一個元素。下方三分之一處、浮水印、持續存在的標籤和法律文字只需一次進場,然後保持靜止。請勿用複雜的進場動畫動畫化正文/字幕文字——淡入是正確的選擇。
- 請勿 在此決定底層的緩動曲線或場景剪輯——那是
motion-design-principles的範疇。請勿 在這裡編寫 Remotion 組件——請將填好的規格交給remotion-compose。 - 請勿 發明文案。動畫化使用者給你的文字;如果一行文字太長無法停留,請要求他們縮短。
相關技能 - 交接,而非吸收
此技能負責文字時機和閱讀性。相鄰的決策屬於相關技能;從它們那裡獲取資訊,而非在此重做它們的工作。
motion-design-principles- 剪輯、非文字動態的節奏,以及緩動曲線 (加速度形狀)。從那裡取得曲線並在此應用於文字。video-storyboard- 存在哪些場景和鏡頭,以及它們的順序。先取得場景列表;此技能僅負責計時一個場景內的文字。motion-color-and-light- 文字相對於背景的顏色、對比度和可讀性。如果文字因顏色而難以閱讀,那是該技能的職責,而非在此更改時機。product-demo-director- 產品示範的整體敘事和節拍列表。它決定什麼標題卡說什麼以及何時出現;此技能計時如何動畫化每個標題卡。sound-and-music-sync- 讓文字揭示落在節拍或強拍上。將holdStart@/ 進場幀數交給它,以便揭示可以對齊到音軌。social-video-formatter- 調整字幕軌的大小和裁切以適應平台 (9:16 TikTok、1:1 訊息流、安全區域)。在此處一次性設定時機;讓該技能根據平台重新調整。
計算器
自包含的 Node.js 腳本。儲存為 type_budget.js 並使用 node type_budget.js 執行。無需依賴。編輯 lines 陣列以使用實際文案;它會根據你的 fps 返回每行的幀數預算。
// 動態字型閱讀預算。編輯 lines + fps,然後:node type_budget.js
const fps = 30
const WORDS_PER_SEC = 3.5 // 舒適的螢幕閱讀速度 (~200 wpm)
const FLOOR_SEC = 0.8 // 任何行的最低停留時間,即使只有一個字
const TERM_TAX_SEC = 0.3 // 包含數字/名稱/術語的行每行需加稅
const ENTRANCE_FRAMES = 12 // 0.4s 進場
const EXIT_FRAMES = 9 // 退場 <= 進場
const lines = [
{ text: 'Ship video in one prompt', hasUnfamiliarTerm: false }, // 標題卡
{ text: 'Rendered headless in 4K', hasUnfamiliarTerm: true }, // 字幕:'4K'
{ text: 'Go', hasUnfamiliarTerm: false }, // 英雄字詞
]
const sec2frames = (s) => Math.ceil(s * fps)
function budget(line) {
const words = line.text.trim().split(/\s+/).length
const raw = words / WORDS_PER_SEC + (line.hasUnfamiliarTerm ? TERM_TAX_SEC : 0)
const holdSec = Math.max(FLOOR_SEC, raw)
const holdFrames = sec2frames(holdSec)
const totalFrames = ENTRANCE_FRAMES + holdFrames + EXIT_FRAMES
return { words, holdSec, holdFrames, totalFrames }
}
let cursor = 0
for (const line of lines) {
const b = budget(line)
const enterIn = cursor
const holdStart = enterIn + ENTRANCE_FRAMES
const holdEnd = holdStart + b.holdFrames
const exitOut = holdEnd + EXIT_FRAMES
console.log(`"${line.text}" (${b.words}w)`)
console.log(` hold: ${b.holdSec.toFixed(2)}s = ${b.holdFrames} frames`)
console.log(` total slot: ${b.totalFrames} frames (enter ${ENTRANCE_FRAMES} + hold ${b.holdFrames} + exit ${EXIT_FRAMES})`)
console.log(` Sequence: enter@${enterIn} holdStart@${holdStart} holdEnd@${holdEnd} exitOut@${exitOut}`)
cursor = exitOut
}
console.log(`Total track length: ${cursor} frames (${(cursor / fps).toFixed(2)}s)`)
執行範例輸出
"Ship video in one prompt" (5w)
hold: 1.43s = 43 frames
total slot: 64 frames (enter 12 + hold 43 + exit 9)
Sequence: enter@0 holdStart@12 holdEnd@55 exitOut@64
"Rendered headless in 4K" (4w)
hold: 1.44s = 44 frames
total slot: 65 frames (enter 12 + hold 44 + exit 9)
Sequence: enter@64 holdStart@76 holdEnd@120 exitOut@129
"Go" (1w)
hold: 0.80s = 24 frames
total slot: 45 frames (enter 12 + hold 24 + exit 9)
Sequence: enter@129 holdStart@141 holdEnd@165 exitOut@174
Total track length: 174 frames (5.80s)
解讀:5 個字的標題需要 43 幀的停留、完全可讀時間——而且這是在 12 幀進場和 9 幀退場之外的,所以其實際時段為 64 幀。4 個字的字幕看起來更短,但包含一個數字 (4K),因此術語稅將其停留時間推至 44 幀——儘管字數較少,但比標題稍長。單字 Go 觸發了 0.8 秒的最低值:即使一個字也需要一拍。這些 enter@ / holdStart@ / holdEnd@ / exitOut@ 幀數可以直接放入 remotion-compose 作為 <Sequence from={…} durationInFrames={…}> 的邊界——無需猜測文字應該停留多久。
模板:motion-type-spec
填寫此模板並交給 remotion-compose。每個文字行一個區塊。
MOTION TYPE SPEC. [FILL: composition name]. fps: [FILL] canvas: [FILL: 1920x1080 / 1080x1920]
LINE 1
Copy (verbatim): [FILL]
Hierarchy role: [FILL: headline / subtitle / caption / lower-third / CTA / hero-word]
Size: [FILL]px Weight: [FILL: 400 / 700 / variable axis]
Reading budget: [FILL]w -> hold [FILL]s = [FILL] frames (from type_budget.js)
Entrance: [FILL: fade / rise+fade / word-by-word / pop / wipe] over [FILL: 8-15] frames
Exit: [FILL: fade / cut-on-edit] over [FILL: <=entrance] frames
Property animated: [FILL: opacity / opacity+translateY / scale] (NO font-size/width)
Accent (max one): [FILL: none / weight 400->700 on word "X" / scale-up on "X"]
Frame numbers: enter@[FILL] holdStart@[FILL] holdEnd@[FILL] exitOut@[FILL]
…
缺失部分
對於長內容,此模板通常會包含更多的文字行區塊。請根據需要填寫其餘部分。