倒數計時影片 是做什麼的?
製作出一個能在精確時刻結束的倒數計時。整個技巧的核心只有一個概念:從影格(或單調遞增的時間戳)推算出剩餘時間,然後格式化並動畫顯示那個數字——絕對不要用 setInterval 來倒數。涵蓋直播「即將開始」畫面、產品發售/限時搶購倒數計時,以及循環背景。
使用時機
- 直播「即將開始」/「馬上回來」畫面,搭配 5–10 分鐘的計時器。
- 產品發售和限時搶購倒數計時(
dd:hh:mm:ss格式,倒數至目標日期;營造急迫感時使用mm:ss格式)。 - 任何需要數字在精確影格上歸零的動畫計數器。
唯一規則:從時鐘驅動數字,絕不使用 tick 計數
使用 setInterval(fn, 1000) 的倒數計時會漂移——回呼函數至少會延遲 1000 毫秒才觸發,不會完全精準,而且當分頁被節流或渲染影格變慢時,誤差會累積。常見的狀況是每分鐘大約會損失 1 秒。修正方法對於渲染影片和即時網頁都相同:將剩餘時間計算為權威時鐘的純函數,這樣即使某個影格延遲了,下一個影格也會自動修正,而不是累積誤差。
| 情境 | 權威時鐘 | 剩餘時間 |
|---|---|---|
| Remotion 渲染 | useCurrentFrame() | 總時間 − 影格/影格率 |
| 網頁(即時頁面) | performance.now() 或 Date.now() | 目標毫秒數 − 現在時間 |
| 倒數至固定日期 | Date.now() | 目標時間戳 − Date.now() |
絕不要將剩餘時間儲存在狀態中並遞減它。請在每個影格重新計算。
影格精確的倒數計時(Remotion → MP4)
useCurrentFrame() 是確定性的:影格 N 總是在 N/fps 秒時被渲染。從總時間減去它,計時器天生就是無漂移的。
import { useCurrentFrame, useVideoConfig } from "remotion";
export const Countdown: React.FC<{ durationInSeconds: number }> = ({
durationInSeconds,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
// 剩餘秒數,限制在 0 以上——使用 ceil 讓「1」在整個秒數內顯示
const elapsed = frame / fps;
const remaining = Math.max(0, Math.ceil(durationInSeconds - elapsed));
return <div style={{ fontVariantNumeric: "tabular-nums" }}>{formatMMSS(remaining)}</div>;
};
durationInSeconds 是一個屬性,不是常數——同一個組合可以透過改變一個輸入參數,來渲染一個 5 分鐘的串流計時器或一個 30 秒的銷售計時器。將組合的 durationInFrames 設定為 durationInSeconds * fps,這樣影片的長度就會剛好等於倒數時間。
網頁計時器(即時頁面,requestAnimationFrame)
對於即時頁面,使用 performance.now() 驅動一個 requestAnimationFrame 循環,並且只在顯示的秒數改變時才重新繪製(不需要為了每秒更新 1 次的數字而每秒鐘執行 60 次無謂的運算):
const start = performance.now();
let lastShown = -1;
function tick(now) {
const remaining = Math.max(0, Math.ceil(duration - (now - start) / 1000));
if (remaining !== lastShown) { render(remaining); lastShown = remaining; }
if (remaining > 0) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
倒數至固定日期的實現方式相同,只是改用 Date.now():Math.max(0, target - Date.now())。請參閱 references/countdown-cookbook.md 以取得包含時區處理的完整日期倒數版本。
時間格式化
使用除法與模數將剩餘秒數整數分解成各個欄位,然後補零。根據持續時間選擇合適的格式——當分鐘數才是重點時,絕不要顯示 00天 00時 05分。
const pad = (n) => String(n).padStart(2, "0");
const formatMMSS = (s) => `${pad(Math.floor(s / 60))}:${pad(s % 60)}`;
const formatDHMS = (s) => {
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600);
const m = Math.floor((s % 3600) / 60), sec = s % 60;
return `${pad(d)}:${pad(h)}:${pad(m)}:${pad(sec)}`;
};
| 格式 | 用途 | 隱藏欄位的時機 |
|---|---|---|
mm:ss | 直播「即將開始」、短時計時器(<1小時) | — |
hh:mm:ss | 限時搶購、同日活動 | 當 dd 為 0 時隱藏 |
dd:hh:mm:ss | 產品發售 / 多日倒數 | 隱藏前導零單位以簡潔呈現 |
總是設定 font-variant-numeric: tabular-nums(或使用等寬數字字型),這樣數字才能維持等寬,版面配置也不會在數字變化時跳動。
數字滾輪/翻牌轉場
單純的文字數字切換看起來很廉價;滾動的「里程表」外觀能提升倒數計時的質感。將每個數字渲染成 0–9 的垂直長條,並透過 -數字 × 數字高度 平移。動畫位置,而不是文字內容,這樣變化才會是平滑的滑動,而不是突然的彈出。
.reel { height: 1em; overflow: hidden; }
.reel .col { transition: transform .45s cubic-bezier(.22,1,.36,1); }
/* col 包含堆疊的 0..9;透過 -value em 偏移 */
在 Remotion 中,透過影格來驅動長條偏移,而不是 CSS 過渡(這樣才能確保確定性)。翻牌卡樣式(上半部向下摺疊覆蓋下半部)和每位數的滾輪效果,在 references/digit-flip.md 中都有詳細說明並附有可運行的元件。
循環背景
串流畫面會持續數分鐘,因此背景必須無縫循環。建立一個週期能均分時間線的動畫(例如一個週期為 loopFrames 的漸層或粒子飄移),並以 frame % loopFrames 的方式讀取它,這樣在循環點就不會有可見的接縫。保持低對比度和緩慢速度——數字才是主角;背景只是氛圍。可在 references/countdown-cookbook.md 中找到實作方法。
輸出檢查清單
- 剩餘時間每個影格都從
useCurrentFrame()/performance.now()重新計算——沒有setInterval遞減。 - 計時器在精確的目標影格上顯示
00:00;durationInFrames === durationInSeconds × fps。 - 持續時間是一個輸入屬性,不是硬編碼的常數。
- 數字使用
tabular-nums;版面配置不因數字變化而移動。 - 格式符合時間跨度(
mm:ssvsdd:hh:mm:ss);當前導零單位造成干擾時將其隱藏。 - 背景循環週期均分時間線;循環點無接縫。
交付與驗證(渲染靜止幀 → MP4)
打包好的輔助腳本(
scripts/):使用scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png拼貼你的靜止幀,然後使用scripts/probe-mp4.sh out.mp4 [WxH] [fps]驗證編碼。參閱scripts/README.md。
倒數計時影片以 Remotion 組合(<Composition> + zod schema + defaultProps,durationInSeconds 為屬性)的方式交付——計時器是 useCurrentFrame() 的純函數,絕不使用 setInterval / Date.now() / Math.random()。設定 durationInFrames = durationInSeconds * fps。交付產物 = out/*.mp4 + 專案。預設為 9:16 垂直(1080×1920)。
驗證循環 — 渲染靜止幀 → 檢查 → 編碼。 整個重點在於影格精確度,所以在編碼之前,請先檢查精確影格上的數字值。
# 開始 / 中間 / 結束的靜止幀 —— 使用實際傳入的屬性(你將渲染的真實持續時間)
npx remotion still Countdown out/f-start.png --frame=0 --props='{"durationInSeconds":300}'
npx remotion still Countdown out/f-mid.png --frame=N --props='{"durationInSeconds":300}'
npx remotion still Countdown out/f-end.png --frame=L --props='{"durationInSeconds":300}' # L = durationInFrames-1
# 檢查每個 PNG —— 數字必須完全符合 frame/fps 的數學計算:
# - 在影格 N,顯示值 = ceil(durationInSeconds - N/fps)(例如 300 秒 @ 30fps,影格 4500 → 150 秒 → "02:30")
# - 最後一個影格顯示 00:00(或結束狀態維持);無漂移,無差一秒的誤差
# - tabular-nums:版面配置在影格之間不跳動
# - 9:16 安全區域:計時器 + 品牌區塊需避開頂部約 12% 和底部約 20-35% 的 UI 以及右側的操作欄
npx remotion render Countdown out/countdown.mp4 --props='{"durationInSeconds":300}' # 確認數字無誤後再編碼
npx remotion render Countdown out/demo.gif --codec=gif # README 展示片段
使用 npx remotion compositions 確認 durationInFrames/fps,然後計算你想檢查的任何秒數所對應的影格。
完成前:
- 靜止幀在影格 0、中間和最後一個影格都能乾淨渲染——沒有錯誤。
- 影格 N 的數字值完全等於
ceil(durationInSeconds - N/fps);最後一個影格在目標影格上顯示 00:00(無漂移 / 無差一秒的誤差)。 - 計時器 + 品牌區塊在每個檢查影格上都保持在 9:16 安全區域內。
- 僅由影格驅動——沒有
setInterval/Date.now()/ 計時器;使用了tabular-nums確保版面穩定。 - 傳入的
durationInSeconds(不僅是defaultProps)能正確渲染;已編碼 MP4,GIF 為選項。
參考檔案
references/countdown-cookbook.md— 完整的可執行 Remotion<Countdown>(可設定持續時間、mm:ss/dd:hh:mm:ss、結束狀態維持)、包含時區處理的倒數至固定日期網頁元件、「即將開始」串流畫面版面(品牌區塊 + 社交帳號 + 循環漸層背景),以及無縫循環背景模式。references/digit-flip.md— 兩種確定性的數字動畫元件:里程表滾輪(由影格驅動的垂直 0–9 長條)和翻牌卡(上半部摺疊)效果,以及它們各自適合用於產品發售、銷售或串流的時機。