Communitygithub.com

RevanthThota55/FreeFrame-Studio

將 Apple 的介面設計與流暢物理動畫方法,轉化為網頁實作。適用於建置或審查手勢驅動 UI、彈簧動畫、拖曳/滑動/底部面板互動、動量與可中斷轉場、半透明材質與深度、排版(光學尺寸、字距、行距)、減少動畫偏好設定,以及 Apple 風格介面背後的設計基礎(反饋、空間一致性、克制)。

FreeFrame-Studio 是什麼?

這個技能將 Apple 在 WWDC(特別是 2018 年的《設計流暢介面》)中提出的介面設計原則,轉化為具體的網頁開發實作指南。它核心倡導「介面應從螢幕當下數值開始運動、繼承使用者速度、投射動量,並能在任何瞬間被抓取與反向」,而彈簧動畫(Spring Animation)正是實現這種自然感的關鍵工具,因為它本質上可中斷且感知速度。技能涵蓋了從降低延遲、直接操控、可中斷性、動量投射、空間一致性到材質與深度等 13 個面向,並提供了對應的 CSS、JavaScript 及 Motion/Framer Motion 等網頁函式庫的程式碼範例。這套知識適用於所有需要建置手勢驅動、流暢動畫與 Apple 風格介面的網頁開發者或 AI 代理工具,無論是使用 Claude Code、Cursor 還是 Codex,都能直接套用這些原則來審查或生成更具「生命力」的使用者介面。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/RevanthThota55/FreeFrame-Studio/tree/HEAD/.claude/skills/apple-design

在你喜歡的 AI 中提問

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

說明文件

Apple 設計

初始回應

當此技能首次被叫用,且沒有特定問題時,只回應:

我已準備好協助你在網頁上建置流暢的 Apple 風格介面。我的知識源自 Apple 的 WWDC 設計講座,並已轉化為適用於網頁的內容。

在使用者提問之前,請勿提供任何其他資訊。

Apple 如何打造那些不再像電腦、反而像是你自身延伸的介面。這些知識來自 Apple 的 WWDC 設計講座——主要是《設計流暢介面》(WWDC 2018)——經由提煉並轉化為網頁平台(CSS、Pointer Events、requestAnimationFrame、像 Motion/Framer Motion 這類的彈簧函式庫)。

主軸:當一個介面的動畫從螢幕上的當前數值開始、繼承使用者的速度、將動量向前投射,並且可以在任何瞬間被抓住與反向時,它就會感覺是活生生的。彈簧(Spring)是讓這一切變得自然的工具,因為它們本質上可中斷且能感知速度。

核心概念

「當我們將介面與我們的思考與移動方式對齊時,神奇的事就發生了——它不再感覺像一台電腦,而是感覺像我們自身的無縫延伸。」

一個介面要流暢,就必須像物理世界一樣運作:東西即時回應、連續移動、攜带动量、在邊界處產生阻力,並且可以在運動中途被重新導向。以下所有內容都是為了更接近這個目標。

Apple 將設計視為滿足四種人類需求:安全感/可預測性、理解、成就與喜悅。 這裡的每一條規則都是為了服務其中一項。

1. 回應——消除延遲

延遲出現的那一刻,直接操作的感覺就會「急遽下降」。回應是其他一切的基礎。

  • 在指標按下時回應,而不是在放開時。 按鈕被按下的瞬間就應該給予回饋。等待 click/觸摸抬起才顯示回饋會感覺很遲鈍。
  • 警覺每一毫秒的延遲。 檢查 debounce、人為計時器、轉場等待時間,以及約 300ms 的點擊延遲。輸入路徑上任何非必要的東西都是一種倒退。
  • 在互動進行中,回饋必須是連續的,而不僅在結束時。 對於拖曳、滑桿或抽屜,整個過程都要讓 UI 與指標 1:1 地更新——絕對不要只在手勢完成時才做動畫。
/* 回饋存在於按壓瞬間,且是即時的 */
.button:active {
  transform: scale(0.97);
  transition: transform 100ms ease-out;
}

2. 直接操控——1:1 追蹤

「觸摸與內容應該同步移動。」

當使用者拖曳某個東西時,它必須緊貼在手指下——並且要考慮到從哪裡抓住它的偏移量。在抓住時將元素對齊到中心會立刻打破這個幻覺。

  • 使用 Pointer Events 搭配 setPointerCapture,這樣即使指標離開元素邊界,追蹤也能持續。
  • 追蹤一個簡短的速度/位置歷史記錄(最近幾次的 pointermove 事件),而不僅僅是當前點——你會在放開時需要速度。
el.addEventListener('pointerdown', (e) => {
  el.setPointerCapture(e.pointerId);
  const grabOffset = e.clientY - el.getBoundingClientRect().top; // 尊重他們抓住的位置
  // ...追蹤位置 + 時間戳的歷史記錄以供速度使用
});

3. 可中斷性——最重要的單一原則

「思考與手勢是同步發生的。」

每個動畫都必須能在任何時刻被中斷與重新導向。使用者必須能夠在半空中抓住一個正在移動的元素並反向它,而不需要等待動畫完成。一個正在關閉的模態視窗,如果使用者再次抓住它,應該要跟著手指移動——而不是先完成關閉,然後再重新打開。

  • 絕不要在轉場期間鎖住輸入。
  • 永遠從當前呈現(presentation)的值開始動畫,而不是目標值。 中斷時,讀取元素畫面上的即時變形數值,並從那裡開始新的動畫。從邏輯/目標值開始會導致明顯的跳動。
  • 對任何由手勢驅動的互動,避免使用 CSS transitions 和 @keyframes——它們無法被順利地抓住並在中途反向。彈簧動畫預設從當前數值開始,這正是中斷所需要的。
  • 當手勢反向時,要混合速度——不要突然切斷它。 在反向點用一個動畫取代另一個會產生速度上的不連續,就像一道「磚牆」。能夠在重新設定目標時繼承速度的彈簧函式庫可以避免這個問題。(這就是 iOS 的 additive animations 原生做法;在網頁上,選擇一個能從當前速度重新設定目標的彈簧函式庫。)
  • 將 2D 運動分解為獨立的 X 和 Y 彈簧。 對一個 2D 距離使用單一彈簧,當 X 和 Y 的速度不同時會產生不同步。

4. 行為重於動畫——使用彈簧

「將動畫想像成你與物體之間的對話,而不是由介面預先編寫好的腳本。」

一個預先編寫好、固定時長的動畫無法回應新的輸入。但彈簧可以——新的輸入只是改變了目標,而運動保持連續。對於任何使用者會觸碰到的東西,都應該使用彈簧。

Apple 特意將物理三元組(質量/剛度/阻尼)替換為兩個對設計師更友善的參數。以這兩個參數來思考:

  • 阻尼比(Damping ratio)——控制過衝(overshoot)。1.0 = 臨界阻尼,無彈跳,平穩收斂。< 1.0 = 會過衝並震盪。數值越低越有彈性。
  • 響應時間(Response)——值達到目標的速度,以秒為單位。數值越低越乾脆。這不是「持續時間」——彈簧沒有固定時長;其穩定所需的時間是從參數中浮現出來的。

預設值:

  • 大多數 UI 從 阻尼 1.0(臨界阻尼)開始——優雅且不干擾。
  • 僅在手勢本身攜带动量時(例如輕彈、拋擲、拖曳放開),才加入彈跳(阻尼 ~0.8)。一個剛淡入的選單有過衝會感覺不對勁;但你輕彈過去的卡片有過衝就感覺很自然。

Apple 實際出貨的數值:

互動類型阻尼響應時間
移動 / 重新定位(例如子母畫面)1.00.4
旋轉0.80.4
抽屜 / 底部面板0.80.3

網頁對應(Motion / Framer Motion): bounce + duration 彈簧 API 與 Apple 的阻尼 + 響應時間非常接近。一個安全的內建模組風格是預設到處使用 damping: 1.0 的彈簧;僅在由動量驅動的物理互動中保留彈跳。

import { animate } from 'motion';

// 臨界阻尼預設(無過衝)
animate(el, { y: 0 }, { type: 'spring', bounce: 0, duration: 0.4 });

// 動量互動——一點彈跳,僅因為前面有輕彈動作
animate(el, { y: target }, { type: 'spring', bounce: 0.2, duration: 0.4 });

5. 速度交接——拖曳與動畫之間的接縫

當手勢結束時,動畫必須以手指的準確速度繼續,這樣拖曳與動畫之間才不會有可見的接縫。這是讓「流暢」與「普通」之間產生最大差異的細節。

將指標放開時的速度作為彈簧的初始速度傳入。有些彈簧 API 需要相對速度——將其相對於到目標的剩餘距離進行正規化:

relativeVelocity = gestureVelocity / (targetValue − currentValue)

範例:元素在 y=50,目標 y=150(還有 100px 要走),手指移動速度 50px/s → 初始彈簧速度 = 50 / 100 = 0.5。Framer Motion / Motion 直接接受以 px/s 為單位的絕對速度(透過 velocity 選項),所以通常你只需傳入原始值。

6. 動量投射——動畫到手勢要去的方向

「用小輸入產生大輸出。」

不要從放開點快照到最近的邊界。使用速度來投射靜止位置——就像滾動減速一樣——然後捕捉到距離該投射點最近的目標。這就是讓輕彈感覺像是把元素丟出去的原因。

Apple 的準確投射函數(來自《設計流暢介面》的範例程式碼):

// decelerationRate ≈ 0.998 表示正常的滾動感;0.99 表示更乾脆
function project(initialVelocity /* px/s */, decelerationRate = 0.998) {
  return (initialVelocity / 1000) * decelerationRate / (1 - decelerationRate);
}

const projectedEndpoint = currentPosition + project(releaseVelocity);
const target = nearestSnapPoint(projectedEndpoint);   // 從投射位置選擇目標
animateSpringTo(target, { velocity: releaseVelocity }); // 然後交接速度(§5)

注意:物理教科書上的 v²/(2·decel) 不是 Apple 出貨的方式——請使用上述的指數衰減形式。這是良好的底部面板與輪播(Vaul、Embla)中的標準行為。

7. 空間一致性——對稱路徑、固定原點

「如果某個東西以一種方式消失,我們會預期它從原來的地方出現。」

  • 從同一路徑進入與離開。 一個從右側滑入的面板必須從右側關閉。從右側進/從底部出會讓人感覺脫節與困惑。
  • 將互動錨定到其來源。 選單、彈出視窗或面板應該從觸發它的元素處出現——將 transform-origin 設定給觸發元素,這樣按鈕與內容之間的空間關係就很明顯。(這與 popover 從觸發元素而非中心縮放是相同原點意識的概念。)
  • 在可逆轉場上鏡像其緩動函數,使離開路徑與返回路徑匹配(對兩個方向使用反向的 cubic-bézier 控制點)。

8. 在手勢方向提供暗示

人類會從軌跡預測最終狀態。中間的畫格應該要暗示事物將去向何方——「控制中心」的模組會「朝著你的手指向上、向外生長」。讓中間畫格指向結果,而不只是盲目地在兩點之間內插。

9. Rubber-banding(橡皮筋效應)——軟邊界

在邊界處,要逐步產生阻力,而不是突然停止。突然停止感覺像「凍結」;連續的阻力感覺像「有回應,但這裡沒有更多東西了」。應用隨著使用者拖曳越過邊界而增加的阻尼。

// 越過邊界越多,元素跟隨得越少——真實世界的物體在停止前會減速
function rubberband(overshoot, dimension, constant = 0.55) {
  return (overshoot * dimension * constant) / (dimension + constant * Math.abs(overshoot));
}

10. 手勢設計細節(「感覺」檢查清單)

  • 點擊: 在觸摸按下時高亮(即時),在觸摸抬起時提交。在目標周圍加入約 10px 的滯後/命中邊距,並允許透過拖曳離開再回來的方式取消。
  • 拖曳/滑動: 在決定一個方向之前,需要一個小的移動閾值(滯後,約 10px),然後進行 1:1 追蹤。
  • 從第一次移動就平行偵測所有可能的手勢,然後在意圖明確時自信地取消失敗者。避免只回報最終狀態的辨識器(如 swipeleft 類事件)——它們會丟棄你進行連續回饋所需的追蹤。
  • 最小化歧義消除延遲。 雙擊偵測不可避免地會延遲單擊;只有在確實存在雙擊的地方才付出這個成本。

11. 畫格級別的流暢度

流暢度關乎的是畫格裡的內容,而不僅僅是畫格率。

  • 保持每個畫格的位置變化低於感知閾值,以避免閃爍(strobing)現象。
  • 對於非常快的運動,加入微妙的動態模糊/拉伸可以編碼速度,比一個銳利的硬線條更容易閱讀。
  • requestAnimationFrame 是網頁上與顯示器同步的時鐘(Apple 使用 CADisplayLink)。只對合成器友善的屬性進行動畫——transform 和 opacity——並在即將發生運動的地方使用 will-change 提示。

12. 材質與深度——半透明傳達層級

Apple 使用半透明材質作為一個浮動的功能層,在帶來結構的同時不搶走焦點。在網頁上,可以使用 backdrop-filter 來近似。

  • 將導航欄/工具欄/面板建置為半透明層(backdrop-filter: blur() + 半透明背景),讓內容在其下方滾動——而不是佔用固定區塊的不透明條。
  • 材質重量編碼了層級: 較暗/較重的材質區分結構區域(側邊欄);較輕的材質將注意力吸引到互動元素(按鈕)。絕對不要在另一個透明表面堆疊一個淺色透明表面——可讀性會崩潰。
  • 較大的表面應感覺較厚: 比較小的區塊有更強的模糊和更深的陰影。考慮上下文相關的陰影——在繁忙/有內容的背景上使用較重的陰影以區隔開來,在簡單背景上使用較輕的陰影。
  • 變暗以聚焦,分離以保持流暢。 一個模態任務會將表面與一個暗化的遮罩層搭配,並將背景推後/向下。一個並行的、非阻塞的面板則使用半透明和偏移,不使用遮罩層,這樣流程就不會被中斷。對於堆疊的面板,逐步將每個父層變暗並向後推。
  • Vibrancy(活力)讓文字在變化背景上保持可讀。 在模糊/半透明表面上,不要使用純灰色文字——使用更高對比、稍微更重的字重,以及微小的字距增加。將顏色放在實色層上,而不是半透明前景上。
  • 使用滾動邊緣效果,而不是硬分隔線。 與其在固定標題下使用 1px 邊框,不如在浮動金屬與內容交會處放置一個微小的模糊/漸變遮罩——僅在浮動 UI 實際與內容重疊的地方。
  • 物質化,而不只是淡入。 對於玻璃/模糊表面,在進入/離開時同時動畫模糊半徑與縮放,讓表面讀起來像一個真實的物質抵達,而不是單純的不透明度淡入。
.toolbar {
  background: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(20px) saturate(180%);
  border-top: 1px solid rgba(255, 255, 255, 0.4); /* 明亮的頂部邊緣 = 光線照射到材質上 */
}

13. 多模態回饋——動畫 + 聲音 + 觸覺

結合感官的三個規則(來自《設計音訊觸覺體驗》):

  1. 因果關係——回饋的原因必須顯而易見。在實際的因果事件上觸發它(切換開關、項目就定位),並使其特徵與動作的物理性相匹配。
  2. 和諧——視覺、聲音與觸覺必須在同一畫格觸發。它們之間的延遲會摧毀這個幻覺。不要讓一個...

…

[內容因長度限制而截斷]

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

相關技能