倒數計時影片
製作一個能夠在正確時間點精確結束的倒數計時。整個技巧的核心概念是:根據影格(或單調的時間戳記)推算出剩餘時間,然後格式化並將該數字製作成動畫——絕不使用 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/幀率 秒的精確時間點被渲染。從總時間中扣除,計時器自然就不會漂移。
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 是一個屬性 (prop),不是常數——同一個組合 (composition) 可以透過改變一個輸入,來渲染 5 分鐘的串流計時器或 30 秒的銷售計時器。請將組合的 durationInFrames 設定為 durationInSeconds * fps,這樣影片長度就會與倒數時間完全相同。
網頁計時器(即時頁面,requestAnimationFrame)
對於即時網頁,使用 requestAnimationFrame 驅動一個循環,並從 performance.now() 取得時間,只在顯示的秒數改變時才重新繪製(對於 1 Hz 的數字,不需要每秒更新 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。
時間格式化
使用除法與取餘數將剩餘秒數整數分解成各個欄位,然後補零。根據持續時間選擇合適的格式——當只需要分鐘時,不要顯示 00d 00h 05m。
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;透過 -值 em 來位移 */
在 Remotion 中,透過影格來驅動條帶偏移量,而不是 CSS 過渡效果(以確保確定性)。翻轉卡風格(上半部向下摺疊覆蓋下半部)和每個數字的滾輪效果,都有詳細說明和可執行元件,請參閱 references/digit-flip.md。
循環背景
直播畫面會持續數分鐘,因此背景必須無縫循環。建立一個週期能整除時間軸的運動(例如,週期為 loopFrames 的漸層或粒子漂移),並以 frame % loopFrames 讀取,這樣在接縫處就不會有可見的斷裂。保持低對比度和慢速——數字才是主角,背景只是氛圍。配方在 references/countdown-cookbook.md。
輸出檢查清單
- 剩餘時間在每個影格從
useCurrentFrame()/performance.now()重新計算——沒有setInterval遞減。 - 計時器在預期的精確影格顯示
00:00;durationInFrames === durationInSeconds × fps。 - 持續時間是輸入的屬性 (prop),不是硬編碼的常數。
- 數字使用
tabular-nums;佈局不會因數字變化而移動。 - 格式符合時間跨度(mm:ss vs dd: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 [寬x高] [幀率]驗證編碼。請參閱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——數字必須完全符合影格/幀率的數學計算:
# - 在影格 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 條帶)和翻轉卡(上半部摺疊)效果,以及各自適合用於發佈會、限時搶購還是直播的情境說明。