Dynamization Core(繁體中文)
這是
SKILL.md的繁體中文版。英文正典在 repo 根目錄的SKILL.md。 runtime adapter(css/waapi/luau/porting)與spring.md只有英文版,在references/。
動態是時間的語言:東西從哪裡來、往哪裡去、現在能不能碰它。 亮度是空間的語言:什麼在上面、什麼是活的、現在該看哪裡。
兩者分開用,各自只做完一半的事。「浮起來」不是把東西往上移 4px,而是 位移 + 更大更柔的陰影 + 更亮的表面,三件事同時發生。 這樣大腦才會讀成 那個東西朝我靠近了。
感覺「生硬」或「沒層次」的介面,幾乎從來不是敗在曲線不夠漂亮。它們是違反了物理直覺, 或是該動三個屬性的時候只動了一個。
這套語言包不擁有任何 API。 底下每一條判斷都寫成人眼可以驗證的數字——秒數、像素距離、 阻尼比、亮度階。執行環境是你在 §2 挑一次、之後就不用再想的細節。
0. Language
references/ 底下每個檔案都是英文。判斷章節(本檔案、feel、contrast、recipes、
pitfalls、errata)的完整翻譯放在 i18n/<locale>/:
| Locale | Path |
|---|---|
| 繁體中文 | i18n/zh-TW/ |
| 日本語 | i18n/ja/ |
| 한국어 | i18n/ko/ |
| Español | i18n/es/ |
如果你正在用其中一種語言回覆使用者,就去讀該語系的檔案,不要讀英文版。
轉接層(references/adapters/)刻意只有英文——它們大多是程式碼和 API 識別字,
翻譯只會製造雜訊和版本漂移。
1. 規格詞彙
這套語言包裡的一切,都用六個詞寫成。學一次就好;能被移植的就是這些。
| Term | 意思 | 寫法 |
|---|---|---|
| dur | 持續多久,單位是秒 | 0.25s |
| curve | out(先快後收)、in(先慢後加速)、inout、linear |
out |
| spring(Dv, b) | 用視覺時長 Dv(秒)與彈跳量 b(0–1)描述的彈簧 |
spring(0.3, 0.15) |
| travel | 位移,單位 px,從東西實際所在的位置起算 | y −4px |
| lumin | 一階亮度變化,用表面值或陰影層級來表示 | surface +1 tier |
| stagger | 同層元素之間的間隔,單位是秒 | 0.04s |
spring(Dv, b) 是撐起整套的關鍵。Dv 是這段運動看起來花了多久,不含收尾的餘震——
正因為如此,彈簧和 tween 才能被放在一起比較。b = 0 是完全不過衝;b = 1 是彈到誇張。
在設計討論裡永遠不要用剛性/阻尼去描述一個彈簧;沒有人腦中浮得出那個畫面。
要換算就在邊界上換 →
references/spring.md。
2. 挑一個執行環境,然後別擋它的路
去讀你所在執行環境的轉接層。讀一個就好,不要全部讀。
| 執行環境 | 轉接層 | 層級 |
|---|---|---|
純 CSS——transitions、keyframes、linear() easing、view transitions |
references/adapters/css.md |
3 |
Web Animations API——element.animate()、ScrollTimeline |
references/adapters/waapi.md |
2 |
Luau / Roblox——TweenService、TweenInfo、RunService 彈簧 |
references/adapters/luau.md |
2–1 |
| 其他任何東西——遊戲引擎、原生工具組、動畫函式庫 | references/adapters/porting.md |
— |
層級是執行環境裡唯一會改變你設計、而不只是改變語法的東西:
| 層級 | 這個執行環境做得到 | 對你的設計代表什麼 |
|---|---|---|
| 1 | 中斷動畫,並把速度帶過中斷點 | 這套語言包裡寫的全部照用 |
| 2 | 可以中斷,但會從靜止重新開始(速度歸零) | 縮短反向動作;優先用 b ≤ 0.15,讓重啟不會被看成一頓 |
| 3 | 只能跑完,或中斷時直接跳位 | 使用者能重複觸發的東西一律 dur ≤ 0.2s;用取樣曲線假造彈簧 |
多數執行環境預設是第 2 層,只有你自己手刻彈簧積分器時才會升到第 1 層。 你那個環境的轉接層會告訴你是哪一層,以及互動真的需要時該怎麼往上爬一層。
先問自己到底需不需要函式庫或彈簧。 只有一個元素、一個狀態、不會重複觸發的話, 普通 tween 就夠了。彈簧的複雜度是靠可中斷、速度接手、手勢接手換來的。 這三樣你一樣都沒用到,就別付那個代價。
3. 五條金律
寫任何動畫之前先把這五條過一遍。違反其中一條,成品就會怪怪的,而且別人通常說不出哪裡怪。
1. 位置和尺寸用彈簧,不透明度和顏色用 tween
這不是品味問題,也不是某個函式庫的一家之言——它是從大腦怎麼分類眼前所見推導出來的。
| 在動的是什麼 | 用什麼 |
|---|---|
位置、旋轉、傾斜——x y rotate 那一家子 |
spring(0.28, 0.2)——輕微過衝 |
| scale 家族 | spring(0.27, 0)——永遠不過衝 |
| 不透明度、顏色、模糊,以及其他所有東西 | tween,dur 0.3,curve out |
| 三個以上的 keyframe | tween,dur 0.8,curve inout |
原因:會佔據空間的東西(位置、尺寸)被大腦讀成物體,而物體有質量、有慣性。 不透明度和顏色不是物體——它們只代表你看不看得見它——所以在那裡用彈簧會被讀成閃爍。
推論:scale 永遠不要彈。 會彈的放大讀起來像撞到玻璃。
那四列是一組校準過的數值。不知道該用什麼的時候,原封不動照用。
2. 動畫必須可中斷,而且最好能把速度帶過中斷點
最重要的一條,也是最常被漏掉的一條。人在動畫跑到一半改變主意,是再正常不過的行為。
- 能用彈簧就用:真正的彈簧積分器會帶著當下速度走,所以反向時不會硬生生煞停。 這個——而不是彈跳感——才是彈簧真正贏過 tween 的理由。
- 在第 2/3 層的執行環境上,至少要讀當下的值並從那裡開始動,絕對不要從名目上的起點開始。 每次重新觸發都彈回起點,就是那個經典的抖動。
- 絕對不要用無法整組取消的計時器去串接動畫。要用可以整段取消的序列,或父子編排。
- 任何被中斷時會直接跳到終點狀態的「轉場」機制(很多內建的頁面轉場 API 都是這樣), 都不適合用在使用者能快速重複觸發的地方。
3. 進場和退場不對稱
沒有人應該等一個正要離開的東西。
enter: opacity 0→1, y +8→0 dur 0.25 curve out
exit: opacity 1→0, y 0→+4 dur 0.15 curve in
退場大約跑進場時長的 0.5–0.7×,而且距離更短。要離開的元素不需要走完整條路徑—— 眼睛只需要知道它走了。
4. 時長分級:不同的工作有不同的預算
| 分級 | dur | 用在哪 | 曲線 |
|---|---|---|---|
| 即時回饋 | 0.1–0.15s |
按壓縮放、核取方塊、focus ring | out 或 spring(0.15, 0) |
| 微互動 | 0.15–0.25s |
hover、tooltip、按鈕變色 | out |
| 元件轉場 | 0.25–0.4s |
下拉選單、對話框、摺疊面板、重排 | spring(0.3, 0.15) |
| 頁面/敘事 | 0.4–0.8s |
路由切換、主視覺、多 keyframe | spring(0.5, 0.1) + stagger |
超過 1s |
幾乎肯定是錯的 | 只有載入、環境動態、捲動連動才適合 | — |
hover 動畫不可以超過 0.2s——游標可能早就移開了。 距離比較長的可以跑久一點,但不是線性的:距離加倍大約只換到多 20–30% 的時間, 不是多 100%。
完整推論、彈簧參數對應的人類語義、編排節奏、反模式 →
references/feel.md
5. 一個事件應該讓好幾個屬性往同一個方向變
單一屬性帶的資訊太少。大腦是靠多個屬性一起動,才把它讀成一個物理事件。
| 要表達 | 至少要改變 |
|---|---|
| 浮起/靠近(卡片 hover) | y 往上 + 陰影變大變柔 + 表面變亮 |
| 按壓/凹陷 | scale 縮小 + 陰影收緊 + 內陰影 + 變暗 |
| 被拿起來(拖曳中) | scale 放大 + 大範圍陰影 + 提升深度層序 |
| 聚焦(對話框開啟) | 內容進場 + 背景變暗(遮罩必須先到位) |
| 停用 | 專用的低對比色彩 token(不是 50% 不透明度) |
亮度是狀態訊號;位移是過程。所以亮度要變得比移動快
(大約 0.12–0.15s 對上 0.2–0.35s)。
深色主題下陰影幾乎看不見,所以層級必須改用更亮的表面來表達。 切換主題換掉的是機制,不只是色票。
畫面上有時候還有第三樣東西——材質:剖面線、顆粒、重複的紋路。它不是一個通道,
也不是規格詞彙。它是 lumin 的修飾語,用來強化物件所倚靠的那層底,而且必須比你階梯裡
最小的那一步亮度差還安靜,否則它讀起來會是雜訊而不是材質。它也永遠輸給文字:沒有任何
剖面線壓在連續文字底下,而且在任何有字的區域裡,材質都不能是眼睛最先碰到的東西。
只能拿來強化和點綴 → contrast.md §8。
光學模型、亮暗兩套 token、動畫化各個亮度屬性的成本、無障礙下限 →
references/contrast.md
4. 路由表
按照你眼前的工作去讀對應的檔案。不要全部讀。
| 你正在做的事 | 讀 |
|---|---|
| 想搞懂「自然」是什麼意思;手感就是調不對;有人說它「很生硬」 | references/feel.md ← 時間軸 |
| 層級、陰影、深色模式、聚焦、遮罩、對比 | references/contrast.md ← 空間軸 |
把 spring(Dv, b) 換算成你的執行環境要的那些數字 |
references/spring.md |
| 你想要一個現成的效果來改 | references/recipes.md |
| 在 CSS / WAAPI / Luau 裡寫真正的呼叫 | references/adapters/<runtime>.md |
| 你的執行環境在這裡沒有轉接層 | references/adapters/porting.md |
| 什麼都不動、會卡頓、退場不觸發、反向會抖 | references/pitfalls.md |
| 看起來什麼都沒壞,而你正要說做完了 | references/errata.md ← 出貨前先讀 |
非英文語系:把 feel、contrast、recipes、pitfalls、errata 換成 i18n/<locale>/ 底下的版本。
5. 無障礙不是可選項
任何會移動或縮放大型元素的東西,都必須尊重減少動態效果的偏好設定。 每個平台都有提供;轉接層會告訴你在你的環境裡它叫什麼。
正確的做法不是「把動畫關掉」,而是:停掉位移和縮放,保留不透明度和顏色。 使用者一樣知道畫面變了,只是變成淡入淡出而不是滑動。視差、自動播放的影片和無限循環, 永遠需要額外再寫一條明確的分支。
6. 效能紅線
通用法則:合成很便宜,繪製很貴,版面計算是災難。 每個執行環境都有這三層的某種版本, 連沒有 DOM 的也一樣。
- ✅ 永遠安全:transform(translate / scale / rotate)和 opacity——它們不會引發任何重排
- ⚠️ 繪製(要實測):陰影、圓角、背景色、模糊——小元素沒問題, 大元素或長列表就危險
- ❌ 版面(避開):width、height、top、left、margin、padding、border width—— 動這些會讓周圍所有東西的版面每一幀重算一次
昂貴屬性的標準逃生門,是把兩個狀態都先畫好,然後交叉淡化它們的不透明度,
而不是去動那個昂貴的屬性本身。這招在 contrast.md §6 裡用在陰影上,
而且幾乎可以推廣到 ⚠️ 那一列的所有東西。
7. 這些數字從哪來
這套語言包裡的參數表是一組校準過的數值,對照過主流動畫實作出貨時的預設值, 也對照過它們當初調校時所依據的知覺研究文獻。這裡把它們寫成白話數字, 就是為了讓你永遠不需要為了用這套語言包而去翻某家廠商的文件, 也讓函式庫改 API 的時候,這裡不會跟著爛掉。
當這套語言包和你執行環境自己的文件對某個 API 做了什麼有出入時,以執行環境的文件為準—— 它描述的是它自己的行為。當兩邊對什麼才對味有出入時,優先照這套語言包,然後用自己的眼睛驗證: 一個 0.3s 的彈簧要嘛讀起來像個物體,要嘛不像,這件事不需要引用文獻來當意見背書。