Communitygithub.com

Kreesten-hsh/video

用於製作倒數計時影片、動畫或直播「即將開始」畫面。提供無漂移的幀精確倒數、數字翻轉動畫與時間格式化功能。

video 是什麼?

此技能專為需要產生精確倒數計時影片或動畫的情境設計,特別適用於直播「即將開始」畫面、產品發佈或限時搶購的倒數計時。核心原則是透過幀編號或時間戳記來計算剩餘時間,而非使用 `setInterval`,以避免因瀏覽器節流或渲染延遲導致的時間漂移問題。支援 Remotion (MP4) 與網頁兩種平台,在 Remotion 中利用 `useCurrentFrame()` 實現確定性渲染;在網頁中則使用 `requestAnimationFrame` 搭配 `performance.now()` 進行高效率更新。技能也涵蓋了數字翻轉滾輪動畫、可無縫循環的背景,以及多種時間格式(如 mm:ss 或 dd:hh:mm:ss)。提供了完整的驗證流程,包括渲染靜止幀檢查數字正確性,並確保最終輸出為無漂移的 MP4 或 GIF。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Kreesten-hsh/video/tree/HEAD/.agents/skills/countdown-video

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

倒數計時影片

製作一個能夠在正確時間點精確結束的倒數計時。整個技巧的核心概念是:根據影格(或單調的時間戳記)推算出剩餘時間,然後格式化並將該數字製作成動畫——絕不使用 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,然後計算你想檢查的任何秒數對應的影格。

完成之前:

  1. 靜止幀在影格 0、中間和最後都能乾淨渲染——沒有錯誤。
  2. 影格 N 的數字值完全等於 ceil(durationInSeconds - N/fps);最後一個影格在預期影格顯示 00:00(無漂移 / 無差一秒)。
  3. 計時器 + 品牌在每個檢查的影格都保持在 9:16 安全區內。
  4. 僅由影格驅動——無 setInterval / Date.now() / 計時器;使用了 tabular-nums 因此佈局穩定。
  5. 交付的 durationInSeconds(不僅是 defaultProps)能正確渲染;MP4 已編碼,GIF 可選。

參考檔案

  • references/countdown-cookbook.md — 完整的可執行 Remotion <Countdown>(可配置持續時間、mm:ss / dd:hh:mm:ss、結束狀態保持),帶時區處理的倒數到指定日期網頁元件,「即將開始」直播畫面佈局(品牌區塊 + 社群媒體連結 + 循環漸層背景),以及無縫循環背景模式。
  • references/digit-flip.md — 兩個確定性的數字動畫元件:里程表滾輪(由影格驅動的垂直 0–9 條帶)和翻轉卡(上半部摺疊)效果,以及各自適合用於發佈會、限時搶購還是直播的情境說明。

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

相關技能