HyperFrames
HTML 是影片的唯一真理來源。一個合成就是一個帶有 data-* 屬性來控制時間、一個 GSAP 時間線來控制動畫、以及 CSS 來控制外觀的 HTML 檔案。框架會處理剪輯的可見性、媒體播放和時間軸同步。
方法
在撰寫 HTML 之前,先從高層次思考:
- 什麼 — 觀眾應該體驗到什麼?找出敘事弧線、關鍵時刻和情感節奏。
- 結構 — 有多少個合成?哪些是子合成,哪些是內嵌的?哪些音軌承載什麼內容(影片、音訊、疊層、字幕)?
- 時間 — 哪些剪輯決定持續時間?轉場發生在哪裡?節奏如何?
- 佈局 — 先建立最終狀態。請參閱下面的「佈局先於動畫」。
- 動畫 — 然後使用以下的規則加入動態。
對於小規模編輯(修正顏色、調整時間、新增一個元素),可以直接跳到規則部分。
視覺識別閘門
請依此順序檢查:
- 專案中存在
DESIGN.md? → 讀取它。使用其中確切的顏色、字型、動畫規則和「不該做的事」限制。 - 存在
visual-style.md? → 讀取它。套用其中的style_prompt_full和結構化欄位。(注意:visual-style.md是專案專用檔案。visual-styles.md是包含 8 個命名預設的樣式庫 — 不同的檔案。) - 使用者指定了樣式(例如「Swiss Pulse」、「暗色科技風」、「奢華品牌」)? → 讀取 visual-styles.md 中的 8 個命名預設。產生一個最簡的
DESIGN.md,包含:## Style Prompt(一段)、## Colors(3-5 個十六進位色值及角色)、## Typography(1-2 個字型家族)、## What NOT to Do(3-5 個反模式)。 - 以上皆非? → 在撰寫任何 HTML 之前,先問 3 個問題:
- 情緒是什麼?(爆炸性 / 電影感 / 流暢 / 技術感 / 混亂 / 溫暖)
- 亮色還是暗色畫布?
- 有任何特定的品牌顏色、字型或視覺參考嗎?
然後根據答案產生一個最簡的
DESIGN.md。
每個合成都必須將其調色盤和字型回推到某個 DESIGN.md、visual-style.md 或明確的使用者指示。如果你正在使用 #333、#3b82f6 或 Roboto — 你跳過了這一步。
對於動畫預設、尺寸、進場模式和緩動曲線 — 請遵循 house-style.md。house style 處理如何移動。DESIGN.md 處理看起來像什麼。
佈局先於動畫
將每個元素放在其最顯眼的時刻應該在的位置 — 也就是它完全進入、正確定位、且尚未退出的那一幀。先將其撰寫為靜態的 HTML+CSS。還不需要 GSAP。
為什麼這很重要: 如果你將元素放在其動畫起始狀態(畫面外、縮放為 0、透明度 0),然後透過補間動畫將它們移動到你認為它們應該落地的位置,你就是在猜測最終的佈局。重疊問題在影片渲染之前是看不見的。通過先建立最終狀態,你可以在加入任何動畫之前看到並修復佈局問題。
流程
- 為每個場景找出英雄畫面 — 最多元素同時可見的時刻。這就是你要建立的佈局。
- 為該畫面撰寫靜態 CSS。
.scene-content容器必須使用width: 100%; height: 100%; padding: Npx;並搭配display: flex; flex-direction: column; gap: Npx; box-sizing: border-box來填滿整個場景。使用padding將內容向內推 — 絕對不要在內容容器上使用position: absolute; top: Npx。使用絕對定位的內容容器在內容高於剩餘空間時會溢出。保留position: absolute僅供裝飾性元素使用。 - 使用
gsap.from()加入進場 — 從畫面外/不可見動畫到 CSS 位置。CSS 位置是事實來源;補間動畫描述達到該位置的過程。 - 使用
gsap.to()加入出場 — 從 CSS 位置動畫到畫面外/不可見。
範例
/* scene-content 填滿場景,padding 定位內容 */
.scene-content {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
padding: 120px 160px;
gap: 24px;
box-sizing: border-box;
}
.title {
font-size: 120px;
}
.subtitle {
font-size: 42px;
}
/* 容器填滿任何場景尺寸 (1920x1080, 1080x1920 等)。
Padding 定位內容。Flex + gap 處理間距。 */
錯誤示範 — 硬編碼尺寸和絕對定位:
.scene-content {
position: absolute;
top: 200px;
left: 160px;
width: 1920px;
height: 1080px;
display: flex; /* ... */
}
// 步驟 3:動畫進入這些位置
tl.from(".title", { y: 60, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power3.out" }, 0.2);
tl.from(".logo", { scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.out" }, 0.3);
// 步驟 4:從這些位置動畫出去
tl.to(".title", { y: -40, opacity: 0, duration: 0.4, ease: "power2.in" }, 3);
tl.to(".subtitle", { y: -30, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.1);
tl.to(".logo", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.2);
當元素在不同時間共享空間時
如果元素 A 在元素 B 進入同一區域之前退出,兩者都應該為各自的英雄畫面設定正確的 CSS 位置。時間軸的順序保證它們永遠不會在視覺上共存 — 但如果你跳過佈局步驟,你就不會發現它們因時間錯誤而意外重疊的情況。
什麼算是有意重疊
分層效果(文字後的光暈、陰影元素、背景圖案)和 Z 軸堆疊設計(卡片堆疊、深度層次)是刻意的。佈局步驟的目的是捕捉無意的重疊 — 兩個標題疊在一起、一個統計數字蓋住標籤、內容溢出畫面。
資料屬性
所有剪輯
| 屬性 | 必要 | 值 |
|---|---|---|
id | 是 | 唯一識別碼 |
data-start | 是 | 秒數或剪輯 ID 參照("el-1"、"intro + 2") |
data-duration | img/div/compositions 必要 | 秒數。影片/音訊預設為媒體持續時間。 |
data-track-index | 是 | 整數。同一音軌的剪輯不可重疊。 |
data-media-start | 否 | 來源的修剪偏移量(秒) |
data-volume | 否 | 0-1(預設 1) |
data-track-index 不影響視覺層級 — 請使用 CSS z-index。
合成剪輯
| 屬性 | 必要 | 值 |
|---|---|---|
data-composition-id | 是 | 唯一的合成 ID |
data-start | 是 | 開始時間(根合成:使用 "0") |
data-duration | 是 | 優先於 GSAP 時間線持續時間 |
data-width / data-height | 是 | 像素尺寸(1920x1080 或 1080x1920) |
data-composition-src | 否 | 外部 HTML 檔案路徑 |
合成結構
透過 data-composition-src 載入的子合成使用 <template> 包裝器。獨立合成(主 index.html)不使用 <template> — 它們將 data-composition-id div 直接放在 <body> 中。在獨立檔案上使用 <template> 會從瀏覽器隱藏所有內容並破壞渲染。
子合成結構:
<template id="my-comp-template">
<div data-composition-id="my-comp" data-width="1920" data-height="1080">
<!-- 內容 -->
<style>
[data-composition-id="my-comp"] {
/* 隔離樣式 */
}
</style>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// 補間動畫...
window.__timelines["my-comp"] = tl;
</script>
</div>
</template>
在根中載入:<div id="el-1" data-composition-id="my-comp" data-composition-src="compositions/my-comp.html" data-start="0" data-duration="10" data-track-index="1"></div>
影片與音訊
影片必須設定爲 muted playsinline。音訊始終是一個獨立的 <audio> 元素:
<video
id="el-v"
data-start="0"
data-duration="30"
data-track-index="0"
src="video.mp4"
muted
playsinline
></video>
<audio
id="el-a"
data-start="0"
data-duration="30"
data-track-index="2"
src="video.mp4"
data-volume="1"
></audio>
時間線契約
- 所有時間線都以
{ paused: true }開始 — 播放器控制播放 - 註冊每個時間線:
window.__timelines["<composition-id>"] = tl - 框架會自動巢狀子時間線 — 請勿手動新增
- 持續時間來自
data-duration,而非 GSAP 時間線長度 - 永遠不要建立空的補間動畫來設定持續時間
規則(不可協商)
確定性: 沒有 Math.random()、Date.now() 或基於時間的邏輯。如果你需要偽隨機值,請使用種子 PRNG(例如 mulberry32)。
GSAP: 只動畫視覺屬性(opacity、x、y、scale、rotation、color、backgroundColor、borderRadius、變形)。請勿動畫 visibility、display 或呼叫 video.play() / audio.play()。
動畫衝突: 永遠不要同時從多個時間線動畫同一個元素上的同一個屬性。
沒有 repeat: -1: 無限重複的時間線會破壞捕捉引擎。從合成持續時間計算確切的重複次數:repeat: Math.ceil(duration / cycleDuration) - 1。
同步時間線建構: 永遠不要在 async/await、setTimeout 或 Promise 內部建構時間線。捕捉引擎會在頁面載入後同步讀取 window.__timelines。字型由編譯器嵌入,因此立即可用 — 無需等待字型載入。
絕對不要:
- 忘記註冊
window.__timelines - 使用影片代替音訊 — 始終使用靜音影片 + 獨立的
<audio> - 將影片巢狀在計時 div 內 — 使用非計時的包裝器
- 使用
data-layer(改用data-track-index)或data-end(改用data-duration) - 動畫影片元素的尺寸 — 動畫一個包裝 div
- 對媒體呼叫 play/pause/seek — 框架擁有播放控制
- 建立一個沒有
data-composition-id的頂層容器 - 在任何時間線或補間動畫上使用
repeat: -1— 始終使用有限重複 - 非同步建構時間線(在
async、setTimeout、Promise內部) - 在後續場景的剪輯元素上使用
gsap.set()— 它們在頁面載入時不存在於 DOM 中。請改用時間線內部的tl.set(selector, vars, timePosition),時間位置在剪輯的data-start時間或之後。 - 在內容文字中使用
<br>— 強制換行不考慮實際渲染的字型寬度。自然換行的文字加上<br>會產生一個額外的不必要換行,導致重疊。請改用max-width讓文字自動換行。例外:簡短的顯示標題,其中每個單詞故意獨佔一行(例如 130px 的「THE\nIMMORTAL\nGAME」)。
場景轉場(不可協商)
每個多場景合成必須遵循以下所有規則。違反任何一條都會導致合成失敗。
- 始終在場景之間使用轉場。不允許跳切。沒有例外。
- 始終在每個場景上使用進場動畫。每個元素都透過
gsap.from()動畫進入。沒有元素可以直接完整出現。如果一個場景有 5 個元素,它就需要 5 個進場補間動畫。 - 永遠不要使用出場動畫,除非是在最後一個場景。這表示:在轉場觸發之前,嚴禁使用
gsap.to()將透明度動畫到 0、將元素移出畫面、縮放到 0 或任何其他「離開」動畫。轉場本身就是出場。在轉場開始的瞬間,正在離開的場景的內容必須完全可見。 - 僅限最後場景: 最後一個場景可以淡出元素(例如淡出為黑色)。這是唯一允許使用
gsap.to(..., { opacity: 0 })的場景。
錯誤示範 — 轉場前的出場動畫:
// 禁止 — 這會在轉場可以使用場景之前清空它
tl.to("#s1-title", { opacity: 0, y: -40, duration: 0.4 }, 6.5);
tl.to("#s1-subtitle", { opacity: 0, duration: 0.3 }, 6.7);
// 轉場在空白畫面上觸發
正確示範 — 僅進場,轉場處理出場:
// 場景 1 進場動畫
tl.from("#s1-title", { y: 50, opacity: 0, duration: 0.7, ease: "power3.out" }, 0.3);
tl.from("#s1-subtitle", { y: 30, opacity: 0, duration: 0.5, ease: "power2.out" }, 0.6);
// 沒有出場補間動畫 — 在 7.2 秒的轉場處理場景切換
// 場景 2 進場動畫
tl.from("#s2-heading", { x: -40, opacity: 0, duration: 0.6, ease: "expo.out" }, 8.0);
動畫護欄
- 第一個動畫偏移 0.1-0.3 秒(不要從 t=0 開始)
- 在進場補間動畫中變化緩動曲線 — 每個場景至少使用 3 種不同的緩動曲線
- 不要在一個場景內重複相同的進場模式
- 避免在深色背景上使用全螢幕線性漸層(H.264 會產生色塊 — 改用放射狀或純色 + 局部光暈)
- 對於渲染的影片:標題 60px 以上,內文 20px 以上,數據標籤 16px 以上
- 在數字欄位上使用
font-variant-numeric: tabular-nums
當沒有提供 visual-style.md 或動畫方向時,請遵循 house-style.md 來設定美學預設值。
字型與素材
- 字型: 直接在 CSS 中撰寫你想要的
font-family— 編譯器會自動嵌入支援的字型。如果某字型不受支援,編譯器會發出警告。 - 為外部媒體加上
crossorigin="anonymous" - 對於
…(已截斷以利翻譯)