解說動畫
一個影片由 src/timeline.ts 中的有序場景列表組成。共有三種類型:
intro:鉤子卡。它的第一行是鉤子,以大字顯示,並隨著語音逐字亮起(它同時也作為自己的字幕)。anim:一個來自元件註冊表的動畫元件,與該場景的旁白台詞同步。outro:結尾卡,包含 AGILabs 標誌、javieraguilar.ai以及來自SETTINGS.articleTitle的文章標題。
撰寫時間軸
import type { Cuts } from './types.ts';
import { L, anim, intro, outro, say, videos } from './narrate.ts';
const build = (): Cuts => ({
main: [
intro('hook', [say(0.4, 'EN hook line.', 'ES hook line.')]),
anim(
'result',
'BarCompare',
{ bars: [{ label: L('With the reason', 'Con el porqué'), value: 14, display: '14/20', highlight: true },
{ label: L('Without it', 'Sin él'), value: 4, display: '4/20' }], max: 20 },
[say(0.3, 'First line.', 'Primera línea.'), say(0, 'Second line.', 'Segunda línea.')],
{ min: 4 },
),
outro('outro', [say(0.4, 'More at javieraguilar.ai.', 'Más en javieraguilar.ai.')]),
],
});
export const VIDEOS = videos(build);
export const TIMELINE = VIDEOS.en.main;
say(at, en, es, spoken?):一行旁白。at是該行在場景中最早可以開始的秒數;各行之間不會重疊(0.35 秒間隔),場景會持續到最後一行結束後再加 0.8 秒,或直到min為止。spoken({ en?, es? })是 TTS 在與字幕文字不同時所朗讀的內容(參閱narration)。- 節拍。 場景中各行的開始時間即為一個節拍。元件在第 i 個節拍時會執行步驟 i(長條 i 增長、節點 i 出現、圖塊 i 彈出),因此請為您想要揭露的每個步驟撰寫一行。在 TTS 之前,節拍來自於長度估算;在
npm run tts之後,它們則來自真實音訊,因此請在配音後重新檢查靜止畫面。 L(en, es):一個雙語螢幕標籤。純字串則兩種語言共用。- 場景 ID 必須是唯一的:行鍵(
<scene>-01,...)用於命名音訊檔案。 anim上的opts.chapter會儲存在場景中,但尚無任何元件會繪製它。- 每次修改後請執行
npm run lint:檢查長度是否在targetSeconds的 ±20 % 範圍內、字幕提示是否低於 0.8 秒或高於 17 字元/秒,以及元件名稱是否在註冊表中遺漏。
元件目錄
所有元件都會從場景接收到 t、beats、dur、lang 和 vertical;以下的 props 則來自時間軸。螢幕上顯示的數字應使用文章中的確切形式:每當文章寫出 "14/20"、"0.0018" 或 "83 %" 時,請優先使用 display 或字串類型的 value,而非計算出來的文字。
| 元件 | 用途 | Props |
|---|---|---|
Counter | 一個從 0 開始遞增的頭條數字,下方帶有標籤 | value: number, label: Label, prefix?, suffix?, decimals? (0-8), beat? (計數開始的節拍索引,預設為 0) |
BarCompare | 2-5 條水平長條,每個節拍增加一條;突出顯示的長條會使用強調漸層 | bars: { label: Label; value: number; display?: string; highlight?: boolean; at?: number }[], max? (刻度頂端,預設為最大值), unit? (當沒有 display 時附加) |
FlowDiagram | 依序出現的方框和箭頭;broken 的邊緣為虛線且顏色較淡,並帶有間隙 | nodes: { id; label: Label; at?: number }[], edges: { from; to; label?: Label; broken?: boolean }[], layout: 'vertical' | 'horizontal' (9:16 裁切請使用 vertical) |
KeyQuote | 關鍵句子,逐詞顯示,強調的字詞會以強調色顯示並加上底線 | text: Label, emphasis?: string[] (忽略大小寫和標點符號進行比對的單字或片語), beat? |
StatGrid | 2-4 個數字圖塊,每個節拍彈出一個 | stats: { value: string; label: Label; at?: number }[] |
長條、節點或圖塊上的 at 會將其固定到某個場景秒數,而非其自身的節拍。沒有節拍或 at 的步驟會在上一個步驟後 0.6 秒開始。每個元件都會被調整,使其在場景結束時完成:最後一幀始終顯示完成狀態。
若要查看整個庫:執行 npm run studio 並開啟 Gallery-vertical,或渲染其靜止畫面。
加入自訂元件
當腳本將某個場景標記為 "bespoke" 時:
- 僅狀態,並附帶測試。 編寫
src/anim/<Name>.state.ts,包含 props 類型和一個純粹的frameState(props, t, beats, dur),它會回傳純數字和字串。那裡不要引入 Remotion 或 React。基於src/anim/steps.ts(sceneTime,stepTimes,fit,progress,beatAt,easeOutCubic)建構它,使其成為完全的:在任何t下都有有限值、progress 欄位在 [0, 1] 範圍內、在第一個節拍之前不可見、在t = dur時為結束狀態。 - 在
test/anim.test.ts的CASES中加入一個案例(以及對其自身行為的測試),然後執行npm test。它會檢查在t < 0、每個節拍、dur以及超過dur時的狀態,並包含遺失、延遲和非常短的節拍情況。 - 繪製它 在
src/anim/<Name>.tsx中:呼叫frameState,在<Stage vertical={...}>內部繪製,使用來自src/brand.ts的品牌代碼(C,GRADIENT,alpha)和 Inter 字型。不要使用新的顏色或字型。 - 註冊它 在
src/anim/index.ts的ANIMS中。保持ANIMS為一個純物件字面量,鍵是名稱:npm run lint會從原始碼中讀取它們。 - 可選擇性地在
src/Gallery.tsx中加入一個範例。如果它是可重複使用的,則應歸回外掛的範本。
生成的圖片。 大多數場景應透過程式碼繪製(來自品牌代碼的 SVG 和 CSS)。如果場景確實需要生成的圖片(背景、基底圖片、紋理化的瞬間),則可以透過影像模型生成,例如 Codex CLI 的圖片生成功能。將其儲存在 public/ 下,使用 staticFile() 載入,保持在品牌色板內,並檢查靜止畫面。
垂直安全區域 (1080x1920)
- 平台 UI 覆蓋頂部約 220 px 和底部約 380 px;兩側保留 72 px。
- 動畫僅在舞台內繪製:從 y = 280(AGILabs 標誌下方)到 y = 1280(兩行字幕上方),寬度為 936 px。
Stage/stageOf()會提供該框。 - 字幕位於舞台下方,最多兩行,每行約 32 個字元,寬度約 900 px。
靜止畫面:仔細檢視
在渲染影片之前,請渲染靜止畫面並閱讀每張 PNG。即使 lint 或建置沒有錯誤,也可能無法顯示溢出、文字被裁剪或錯誤的數字。
npx remotion still src/index.ts Main-EN-vertical out/stills/f0150.png --frame=150
在每個場景的最後一個步驟播放完畢後,為每個場景拍攝一張靜止畫面,此外每約 3 秒(30 fps 下約 90 幀)拍攝一張。檢查:只應有 AGILabs 標誌作為品牌標示、沒有任何內容超出安全區域、沒有文字溢出其框體、每個數字都與文章相符,並且螢幕上的字幕與該時刻動畫顯示的內容一致。