用 HTML 寫影片:HyperFrames 實戰筆記
📝 本文以「我替部落格做一支開場動畫」為主線,邊做邊記——從安裝、合成結構、GSAP 動畫到實戰踩坑與一個關鍵取捨。
一、前言:我想幫部落格做一支開場
事情的起點很單純:我想替「日沐 SunBathe」這個部落格做一段品牌開場 / 動態 hero。
找方案時遇到 HeyGen 開源的 HyperFrames,主打「用 HTML 做影片」——身為前端,這個切入點很對胃口,於是決定實際從零跑一輪,做一支「日沐」太陽升起的開場動畫。
過程比想像中有趣:它的設計哲學(用 HTML 當影片原始碼)、只有實作才會踩到的坑(字體裁切、無限循環壞掉確定性渲染),到最後我才想通的一個關鍵問題——我要的到底是「影片」還是「網頁動畫」? 這篇就把整段經驗記下來。
接下來會帶你看:
- HyperFrames 是什麼、為什麼用 HTML 做影片
- 怎麼安裝、環境要準備什麼
- 一支**合成(composition)**怎麼組成、核心觀念是什麼
- 實際動手做 SunBathe 開場
- 踩到的坑(本篇重點)
- 最後一個關鍵取捨:影片 vs 網頁動畫
全文以「邊做邊記」的第一人稱口吻,程式碼都來自真實做出來的那支合成。
二、HyperFrames 是什麼
一句話:HyperFrames 是 HeyGen 開源的框架,把 HTML / CSS / 媒體 / 可 seek 的動畫,渲染成「確定性」的 MP4 影片。
HTML 就是影片的原始碼
傳統做影片,你的「原始碼」是 .aep(After Effects)、.prproj(Premiere)這類專有二進位格式——只能用特定軟體開、難以版本控制、不可能用程式 diff。
HyperFrames 的核心理念是 HTML is the source of truth:一支影片就是一份 HTML——
- 用 CSS 決定外觀
- 用
data-*屬性描述時間軸(哪個元素何時出現、多久) - 用 GSAP timeline 描述動畫
- 框架負責把這些「seek」成一格一格的畫面,輸出 MP4
於是影片變成純文字、可 git diff、可被程式生成的東西。
為誰設計:AI agent、CLI、自動化
正因為輸入是 HTML、流程走 CLI,它特別適合:
- AI coding agent——用自然語言叫 agent 寫 / 改影片(這也是為什麼它用「skill」的形式發佈)。
- CLI / CI 自動化——
init → lint → preview → render全在命令列,可批次、可進 pipeline。 - hosted workflow——大量、程式化地產生影片(HeyGen 本業就是 AI 影片)。
關鍵詞:「確定性」(deterministic)
這是貫穿全篇的核心,先記著一句:同一個時間點,每次 render 都必須得到一模一樣的畫面。 它約束了你能寫什麼動畫、不能用什麼程式碼——詳細的機制和限制,第四章會拆給你看。
跟 Remotion 差在哪?
| HyperFrames | Remotion | |
|---|---|---|
| 描述方式 | 原生 HTML + data-* | React 元件(JSX) |
| 心智負擔 | 會寫網頁就能上手 | 需熟悉 React |
| 動畫 | GSAP timeline(seek) | React 狀態 + frame 函式 |
| 定位 | 偏 agent / CLI 自動產片 | 偏前端工程師寫程式化影片 |
簡單說:兩者都是「用程式碼做影片」,但 HyperFrames 賭的是「HTML 比 JSX 更通用、更適合給 AI 寫」。
三、安裝與環境
HyperFrames 不是用 npm install 裝的——它本質是一組教 coding agent 的 skill,用 skills CLI 安裝:
npx skills add heygen-com/hyperframes接著會跑一個互動精靈,問你幾件事。我把實際的選擇與理由記下來。
3-1 要裝哪些 skill(共 15 個)
清單分三類,不用全選:
- 核心 4 個(建議必選):
hyperframes、hyperframes-cli、hyperframes-media、hyperframes-registry - 動畫轉接器(挑你會用的):
gsap(官方主推)、css-animations、waapi、animejs、three、typegpu、lottie、tailwind - 特定用途(用到再加):
contribute-catalog(貢獻元件)、remotion-to-hyperframes(從 Remotion 遷移)、website-to-hyperframes(網站轉影片)
💡 全裝也不吃空間——這些是文字說明檔,不是把 GSAP/Three.js 函式庫下載下來。真正的成本是「agent 的 skill 清單變雜」,不是硬碟。
3-2 裝給哪個 agent、裝去哪、怎麼裝
精靈接著問三題,這是我的選擇:
| 問題 | 我選 | 為什麼 |
|---|---|---|
| 裝給哪個 agent | Claude Code(.claude/skills) | 要裝給你實際在用的那個 agent 才生效 |
| Scope | Global(~/.claude/skills) | HyperFrames 是通用工具、跟我的部落格無關,不想汙染 repo 的 git |
| Method | Symlink | 單一來源、skills update 一次更新全部、不佔額外空間 |
反過來說:如果你是想「這個專案專用、而且要讓協作者 clone 就有」,Scope 才選 Project(會一起進版控)。
3-3 環境需求
- Node ≥ 22
- FFmpeg——但有個重要區別:
⚠️ FFmpeg 只有「render 成 MP4」才需要,preview 預覽完全用不到。 所以你想先玩、先預覽,沒裝 FFmpeg 也能開始。要輸出影片時再補:
bashbrew install ffmpeg # macOS
裝完 skill、Node 版本對了,就可以進到下一章——產生第一支合成。
四、一支合成(composition)的解剖
在動手寫動畫前,得先看懂「一支合成長什麼樣」。別手刻檔案,用 init 產生骨架:
npx hyperframes init sunbathe-intro --example blank它會建出這樣的結構(不是只有一個 HTML,還有給 agent 看的說明與設定):
sunbathe-intro/
├── index.html # 主合成(root timeline)— 你主要編輯這個
├── hyperframes.json # registry 與路徑設定
├── meta.json # 專案 metadata(id、name)
├── CLAUDE.md # 給 coding agent 的規則
└── package.json # dev / check / render 指令打開 index.html,blank 範本的核心其實只有三個重點,我們逐一拆解。
4-1 根節點:data-composition-id 宣告一支合成
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="8"
data-width="1920"
data-height="1080"
>
<!-- clips 放這裡 -->
</div>data-composition-id="main":這支合成的識別名,等下要和 timeline 對應。data-duration:整支長度(秒)。所有動畫都活在這個區間內——也是為什麼第六章那個repeat: -1不行。data-width / data-height:畫布尺寸(影片解析度)。
4-2 clip:class="clip" + 時間軸屬性 控制「何時出現」
每個會在時間軸上出現的元素都是一個 clip,需要兩樣東西:
<div class="clip" data-start="0" data-duration="8" data-track-index="0">...內容...</div>class="clip"是必須的——框架靠這個 class 判斷「這元素受時間軸控制」,據此決定它在某個時間點該不該顯示。漏掉它,元素就不受控。data-start/data-duration:這個 clip 何時進場、持續多久。data-track-index:軌道編號(像影片剪輯的圖層),不同軌道可疊放、避免重疊衝突。
💡 把它想成影片剪輯軟體:root 是時間軸,每個 clip 是軌道上的一段素材。
4-3 動畫:window.__timelines 上一條 paused 的 GSAP timeline
clip 解決了「何時出現」,但「出現後怎麼動」是 GSAP 的事。關鍵是:timeline 必須 paused(暫停),並掛到 window.__timelines,用 data-composition-id 當 key:
window.__timelines = window.__timelines || {}
const tl = gsap.timeline({ paused: true }) // ← 一定要 paused
tl.from('.title span', { y: 100, opacity: 0, stagger: 0.16 }, 0.5)
// ...更多 tween...
window.__timelines['main'] = tl // ← key 對應 data-composition-id為什麼是 paused?因為你不是要「播放」它——
4-4 心智模型:不是「播放」,是「seek」
這是整個框架最該先建立的觀念:
- 一般網頁動畫是播放:跟著真實時鐘往前跑(CSS animation、
requestAnimationFrame)。 - HyperFrames 是seek:渲染引擎把這條暫停的 timeline,逐格 seek 到精確時間點(例如第 3.5 秒),截一張圖,再 seek 到下一格⋯⋯拼成 MP4。
render 流程:seek(0.000s) → 截圖 → seek(0.033s) → 截圖 → seek(0.066s) → 截圖 → …理解這點,後面所有「坑」都會變得理所當然:
- timeline 要
paused→ 不然它會自己跑,引擎沒辦法控制要 seek 到哪。 - 不能
repeat: -1→ 無限循環沒有「第 N 格的確定狀態」可算。 - 不能
Math.random()/Date.now()→ 每次 seek 同一時間點都必須得到完全一樣的畫面。
一句話:你不是在「拍動畫」,而是在描述一條「任何時間點都算得出畫面」的函式。確定性,就是從這裡來的。
五、動手做「日沐 SunBathe」開場
觀念到位了,來實際做一支 8 秒開場:暖白背景上,一顆太陽升起並呼吸般脈動,光芒緩緩旋轉,「日沐」兩字逐字浮現。重點是一個方法論——Layout before Animation。
5-1 先擺位,再動畫(Layout before Animation)
官方 skill 一直強調的原則:先用靜態 HTML/CSS 把「最完整可見」的那一影格排好,再加 GSAP 動畫。
- 如果你一開始就把元素擺在動畫起點(offscreen、
scale: 0、opacity: 0),等於是在「猜」最終版面,重疊與跑版要等 render 出來才看得到。 - 先把結束狀態排好,版面問題在加動畫前就能用肉眼修掉。
所以 HTML 就是一個置中的卡片,元素照最終樣子排:
<div
id="hero-card"
class="clip scene-content"
data-start="0"
data-duration="8"
data-track-index="0"
>
<div class="rays" id="rays"></div>
<div class="sun-wrap"><div class="sun"></div></div>
<h1 class="title"><span>日</span><span>沐</span></h1>
<div class="divider"></div>
<div class="brand">SunBathe</div>
<div class="tagline">一個喜歡行光合作用的全端工程師</div>
</div>5-2 用品牌色排好靜態版面(CSS)
容器用 flex 置中、暖白 radial 背景;太陽是一顆 radial-gradient 圓加外發光;文字層墊在太陽之上(z-index):
.scene-content {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 30px;
padding: 120px;
background: radial-gradient(circle at 50% 40%, #fffdf5 0%, #f7f7f5 46%, #eef0e8 100%);
font-family: 'Noto Sans TC', sans-serif;
}
.sun {
width: 540px;
height: 540px;
border-radius: 50%;
background: radial-gradient(circle at 50% 45%, #fff8e6, #f2e9c4 30%, #cdd9af 60%, #9fb682 100%);
box-shadow:
0 0 120px 40px rgba(157, 182, 130, 0.45),
0 0 260px 90px rgba(242, 233, 196, 0.35);
}
.title {
display: flex;
gap: 0.06em;
font-family: 'Noto Serif TC', serif;
font-weight: 700;
font-size: 250px;
color: #3f4a31;
}🎨 這版用的是 matcha 綠系太陽(
#9fb682)。實際放上部落格後我覺得「太綠、不像太陽」,後來把太陽漸層換成暖色金黃——這留到第八章的網頁版再說。
12 道光芒不寫死在 HTML,用固定迴圈動態產生(注意:不能用亂數,否則破壞確定性):
const rays = document.getElementById('rays')
for (let i = 0; i < 12; i++) {
const r = document.createElement('div')
r.className = 'ray'
r.style.transform = `rotate(${i * 30}deg)` // 每道間隔 30°
rays.appendChild(r)
}5-3 加上動畫(一條 paused timeline)
靜態版面就緒,才開始堆 GSAP。全部掛在同一條 paused timeline 上,用第三個參數安排出場時間點:
window.__timelines = window.__timelines || {}
const tl = gsap.timeline({ paused: true })
// 太陽升起(fromTo 保留置中位移,見第六章坑 2)
tl.fromTo(
'.sun-wrap',
{ xPercent: -50, yPercent: -50, y: 130, opacity: 0 },
{ xPercent: -50, yPercent: -50, y: 0, opacity: 1, duration: 1.3, ease: 'power3.out' },
0
)
// 太陽呼吸般脈動(有限次數,見第六章坑 1)
tl.to('.sun', { scale: 1.05, duration: 2.4, ease: 'sine.inOut', repeat: 2, yoyo: true }, 0.6)
// 光芒淡入 + 緩慢旋轉
tl.from('.rays', { opacity: 0, scale: 0.6, duration: 1.6, ease: 'power2.out' }, 0.2)
tl.to('.rays', { rotation: 360, duration: 40, ease: 'none' }, 0)
// 日沐 逐字浮現 → 分隔線展開 → 品牌名 → 標語
tl.from(
'.title span',
{ y: 100, opacity: 0, duration: 0.9, stagger: 0.16, ease: 'power3.out' },
0.5
)
tl.from('.divider', { scaleX: 0, opacity: 0, duration: 0.7, ease: 'power2.out' }, 1.35)
tl.from('.brand', { y: 40, opacity: 0, duration: 0.8, ease: 'power2.out' }, 1.65)
tl.from('.tagline', { y: 30, opacity: 0, duration: 0.8, ease: 'power2.out' }, 1.95)
window.__timelines['main'] = tl幾個對應前面觀念的細節:
gsap.fromvsgsap.to:進場用from(從 offscreen/透明 → 排好的位置),呼應 Layout before Animation——CSS 的位置才是「真相」,tween 只描述「怎麼到達」。- 第三個參數是時間點:
0、0.5、1.35⋯⋯精確排程每個元素何時進場。 stagger: 0.16:讓「日」「沐」兩字依序浮現,而不是同時。
寫到這裡,先別急著看效果——下一步是跑 lint,那正是第六章那一串坑的由來。
六、實戰踩到的坑(本篇重點)
寫完第一版合成,先別急著 preview,第一件事是跑 lint:
npx hyperframes lint我那一版的成績是「1 個 error、4 個 warning」。這一節把每個坑拆成「lint 訊息 → 原因 → 修法」三段——這些幾乎都是「動畫在瀏覽器看起來好好的,render 出來卻壞掉」的那種坑,靠肉眼很難發現,但 lint 一抓一個準。
坑 1:gsap_infinite_repeat(這是會擋下 render 的 error)
lint 訊息:
✗ gsap_infinite_repeat: GSAP tween uses `repeat: -1` (infinite).
Infinite repeats break the deterministic capture engine which seeks
to exact frame times.原因:HyperFrames 不是「播放」動畫,而是把 timeline seek 到每一個精確的影格時間再截圖。repeat: -1(無限循環)沒有明確的結束時間,引擎無法計算「第 N 影格此刻的狀態」,確定性就破了。
修法:改成有限次數,用總長除以單次週期算出來。我原本太陽脈動寫無限:
// ❌ 之前:無限脈動
tl.to('.sun', { scale: 1.05, duration: 2.4, repeat: -1, yoyo: true }, 0.6)
// ✅ 之後:有限次數(3 段 × 2.4s ≈ 7.2s,落在 8s 合成內)
tl.to('.sun', { scale: 1.05, duration: 2.4, repeat: 2, yoyo: true }, 0.6)光芒旋轉也一樣,原本 repeat: -1,改成「單一長 tween」就好——只要它的長度涵蓋整段合成,根本不需要 repeat:
// ✅ 一條 40s 的旋轉 tween,已涵蓋 8s 合成
tl.to('.rays', { rotation: 360, duration: 40, ease: 'none' }, 0)📌 通則:
repeat: Math.floor(總長 / 單次週期) - 1,用Math.floor(不是ceil)確保動畫不超出合成長度。
坑 2:gsap_css_transform_conflict
lint 訊息:
⚠ gsap_css_transform_conflict: ".sun-wrap" has CSS `transform: translate(-50%, -50%)`
and a GSAP tween animates y. GSAP will overwrite the full CSS transform,
discarding any translateX(-50%) centering.原因:我用經典的 transform: translate(-50%, -50%) 把太陽置中,又用 GSAP 動它的 y 做升起。問題是 GSAP 動 y 時會覆蓋整個 transform,那個 -50% 的水平置中就被吃掉,太陽會跑位。
修法:把置中也交給 GSAP,用 fromTo() 搭配 xPercent / yPercent(lint 明說 fromTo 不受此規則限制):
/* CSS 只留定位,移除 transform */
.sun-wrap {
position: absolute;
top: 50%;
left: 50%;
}// 置中(xPercent/yPercent)與升起(y)一起交給 GSAP
tl.fromTo(
'.sun-wrap',
{ xPercent: -50, yPercent: -50, y: 130, opacity: 0 },
{ xPercent: -50, yPercent: -50, y: 0, opacity: 1, duration: 1.3, ease: 'power3.out' },
0
)坑 3:字體會在 render 時 fallback(google_fonts_import / font_family_without_font_face)
lint 訊息:
⚠ google_fonts_import: Composition loads fonts from fonts.googleapis.com.
External font requests fail in sandboxed/offline renders.
⚠ font_family_without_font_face: Font families used without @font-face: noto sans tc, noto serif tc.原因:我直接 <link> 了 Google Fonts。preview 在瀏覽器跑沒問題(抓得到網路字體),但 render 是在 sandbox / 離線環境,抓不到外部字體,「日沐」就會 fallback 成系統字,影片裡的字型跑掉。
修法:把字體換成本地 @font-face 的 .woff2,不要依賴外部網路:
@font-face {
font-family: 'Noto Serif TC';
src: url('./assets/fonts/NotoSerifTC.woff2') format('woff2');
}這也是「preview 過 ≠ render 過」最典型的例子——preview 沒抓到的問題,lint 先幫你抓到了。
坑 4:確定性限制——禁用一切「不可重現」的邏輯
這不是單一 lint 規則,而是貫穿整個框架的鐵則:同樣的 timeline 時間點,每次 render 都必須得到一模一樣的畫面。所以以下通通不能用:
Math.random():要隨機感,就預先算好固定值寫死,或用 index 推導。Date.now()/new Date():時間相關一律不可。- 網路
fetch():外部資料會讓結果不可重現。
我那支開場要產生 12 道光芒,就用固定迴圈而不是亂數:
// ✅ 固定迴圈,每次結果都一樣
for (let i = 0; i < 12; i++) {
const ray = document.createElement('div')
ray.className = 'ray'
ray.style.transform = `rotate(${i * 30}deg)`
rays.appendChild(ray)
}修完的成績
把 error 修掉、warning 評估過後,再跑一次 lint:
◇ 0 error(s), 2 warning(s)剩下的 2 個 warning 是字體那組——因為我這支只要 preview、暫時不 render MP4,就先留著(preview 完全不受影響)。error 一定要清零,warning 看用途決定。
七、預覽 vs 渲染
合成寫好、lint 過了,最後就是「看成果」。這裡有兩個很容易搞混的動作——預覽和渲染完全不同。
7-1 完整 dev loop
官方建議的順序是:
npx hyperframes init my-video # 1. 產生骨架
# 2. 寫 / 改 HTML 合成
npx hyperframes lint # 3. 檢查(必跑,見第六章)
npx hyperframes inspect # 4. 視覺檢查:文字有沒有溢出框、跑出畫布
npx hyperframes preview # 5. 瀏覽器即時預覽
npx hyperframes render # 6. 輸出 MP4
lint抓的是「程式碼層級」的問題(無限 repeat、transform 衝突);inspect是用 headless Chrome 把時間軸 seek 一遍,抓「視覺層級」的問題(文字爆框、元素溢出)。兩個互補。
7-2 preview vs render 的關鍵差別
preview | render | |
|---|---|---|
| 指令 | npx hyperframes preview | npx hyperframes render |
| 需要 FFmpeg | ❌ | ✅ |
| 產物 | 瀏覽器即時畫面 | .mp4 檔 |
| 何時用 | 開發中反覆看效果 | 最終輸出影片 |
- preview 是長駐伺服器:背景跑著、改檔即時熱重載,開發時讓它一直開著就好。
- render 才需要 FFmpeg:所以你想先玩、先看效果,根本不必裝 FFmpeg。
7-3 preview 要用 Studio 專案 URL
一個實測小坑:preview 起來後,別直接開 index.html 的檔案路徑,要用 Studio 專案網址——它才有時間軸 scrub、播放控制:
http://localhost:<port>/#project/<project-name>例如我在 sunbathe-intro 用預設 port 跑,網址就是:
http://localhost:3002/#project/sunbathe-intro八、一個關鍵取捨:你要的是影片,還是網頁動畫?
做到這裡我才意識到一個關鍵問題:我到底要的是「一個影片檔」,還是「網頁上會動的東西」? 這兩者的答案,決定你要不要繼續走 render 這條路。
8-1 HyperFrames 的產物是 MP4,不是網頁成品
- HyperFrames 的終點是 MP4——那份 HTML 合成檔是「原始碼」,不是設計來直接塞進網站的。
- 裡面那些
data-*、class="clip"、window.__timelines、固定 1920×1080、pausedtimeline,全是給渲染引擎 seek 用的機械。 - 如果只是想讓部落格首頁有個「活的」hero,把這份合成 HTML 原封 iframe 進去,會得到一個固定尺寸、暫停不動的東西——完全不對。
8-2 要網頁動畫,就「移植」而不是「轉檔」
正解是:把 CSS 和那條 GSAP timeline 抽出來,做成一個正常的、會自動播放的 Vue 元件。反而比用 HyperFrames runtime 更簡單、更輕、任意解析度都銳利。
我把 SunBathe 的太陽動畫移植成了 HeroSunVisual.vue,過程做了三個針對「網頁」的關鍵調整:
// HeroSunVisual.vue(節錄)
import { ref, onMounted, onBeforeUnmount } from 'vue'
const root = ref<HTMLElement | null>(null)
let ctx: { revert: () => void } | null = null
onMounted(async () => {
// ① 無障礙:尊重「減少動態效果」,直接停在最終狀態
const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
if (reduce || !root.value) return
// SSR 安全:gsap 只在 client 端動態載入
const { gsap } = await import('gsap')
ctx = gsap.context(() => {
const tl = gsap.timeline()
tl.from(sunWrap.value, { yPercent: 28, opacity: 0, duration: 1.3, ease: 'power3.out' }, 0)
// ② 無限循環:網頁沒有確定性限制,repeat:-1 隨便用
gsap.to(sun.value, { scale: 1.05, duration: 2.4, repeat: -1, yoyo: true })
gsap.to(rays.value, { rotation: 360, duration: 60, ease: 'none', repeat: -1 })
}, root.value)
})
onBeforeUnmount(() => ctx?.revert()) // 元件卸載時清掉動畫/* ③ 響應式:用容器單位 cqmin,取代寫死的 px */
.sun-visual {
container-type: size;
}
.sun {
width: 34cqmin;
height: 34cqmin;
}對照「render 版」與「網頁版」的差異——很多 HyperFrames 的限制,在網頁上反而是優勢:
| 面向 | HyperFrames render 版 | 移植的網頁元件版 |
|---|---|---|
| 尺寸 | 固定 1920×1080 px | 響應式 cqmin,跟著容器縮放 |
| 循環 | 禁止 repeat: -1 | 可以無限循環 |
| 播放 | paused + 引擎 seek | 自動播放(真實時鐘) |
| 確定性 | 禁 random / Date.now | 無此限制 |
| 無障礙 | 不適用 | 可加 prefers-reduced-motion |
| SSR | 不適用 | 需把 gsap 留在 client(onMounted) |
8-3 所以——你該轉檔嗎?
| 你要的 | 需要轉檔(MP4) | 做法 |
|---|---|---|
| 網頁上的活動畫(hero) | ❌ | 移植成自動播放 Vue 元件 |
| 一個影片檔(社群分享、對外發) | ✅ | 裝 FFmpeg → render |
一句話:要「影片檔」就 render;要「網頁會動」就移植它的動畫。 我的部落格 hero 屬於後者,所以最後根本沒用到 FFmpeg。
九、小結:什麼時候值得用 HyperFrames
跑完整輪,我對「該不該用 HyperFrames」有了比較具體的判斷。
它真正發光的場景——當你需要的是「程式化、可重現、可自動產的影片檔」:
- 社群短片、產品 demo、開場 / 片頭
- 字幕 + 配音(TTS)影片
- 用 AI agent 或 CI 批次產生大量影片
- 想把影片「原始碼」納入 git 版控、能 diff、能 code review
它不適合的場景——當你要的其實是「網頁上會動的東西」:
- 部落格的互動 / 裝飾動畫、捲動特效
- 這種情況直接寫 GSAP / WAAPI 元件更輕、更銳利、還能響應式(就像我最後對首頁 hero 做的)
最大的收穫
對我來說,這趟最值得的不是「學會一個工具」,而是被逼著想清楚一個常被混淆的問題:
要「影片檔」就用 HyperFrames render;要「網頁會動」就把它的動畫移植成元件。
搞清楚這條分界,比任何 API 都實用。而 HyperFrames 用 HTML 當影片原始碼的這個賭注——讓「做影片」變成一件前端寫程式、AI 能接手的事——確實是個很有意思的方向。
延伸資源: