Communitygithub.com

ZainR525/PersonalWebsiteZainRizvi

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

PersonalWebsiteZainRizvi 是什麼?

此技能將 Apple 在 WWDC 設計講座(尤其是 2018 年《設計流暢介面》)中分享的介面設計原則,轉化為網頁開發者可直接應用的內容。其核心觀點是:一個介面要讓人感到「活著」,必須滿足四個關鍵原則:即時回應(在 pointer-down 時立即提供回饋)、直接操作(拖曳內容時 1:1 跟隨手指)、可中斷性(任何動畫都能被隨時打斷並反向)、以及以彈簧(Spring)為基礎的行為(而非固定時長的動畫)。它提供了具體的 CSS/JavaScript 實作範例,包括使用 Pointer Events 處理拖曳、利用彈簧動畫庫(如 Motion/Framer Motion)達成自然且可打斷的動畫、以及透過速度傳遞與動量投射來消除拖曳後動畫的銜接縫隙。此外,也涵蓋了半透明材質(backdrop-filter)、空間一致性(進出路徑對稱)、橡皮筋效果(柔邊界)、以及多模態回饋等進階細節。此技能適用於任何需要建構高品質、具有 Apple 風格流暢體驗的網頁應用,支援平台包含所有現代瀏覽器。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ZainR525/PersonalWebsiteZainRizvi/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 這樣的彈簧動畫庫)。

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

核心概念

「當我們讓介面與我們的思考和移動方式保持一致時,就會發生神奇的事情——它不再像一台電腦,而開始感覺像是我們身體無縫的延伸。」

當一個介面像物理世界一樣運作時,它就是流暢的:事物即時回應、連續移動、帶有動量、在邊界處產生阻力、並且可以在運動過程中被重新導向。以下所有內容都是為了更接近這個目標。

Apple 將設計框架為滿足四種人類需求:安全感/可預測性、理解、成就感和愉悅感。 這裡的每一條規則都服務於其中一種。

1. 回應 — 消滅延遲

延遲出現的那一刻,直接操作的感覺就會「懸崖式下跌」。回應是其他一切建立的基礎。

  • 在 pointer-down 時回應,而不是在釋放時。 按鈕被按下的瞬間就應顯示高亮。等待 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. 可中斷性 — 最重要的原則

「意念和手勢是同時發生的。」

每個動畫都必須在任何時候可被中斷和重新導向。使用者必須能夠在移動中的元素飛行途中抓住它並反向,而無需等待動畫結束。一個正在關閉的模態框如果使用者再次抓住它,應該跟著手指移動——而不是先完成關閉,然後再重新打開。

  • 永遠不要在轉場期間鎖定輸入。
  • 總是從呈現(當前)數值開始動畫,而不是目標數值。 中斷時,讀取元素在螢幕上的即時 transform 並從該數值開始新的動畫。從邏輯/目標數值開始會導致可見的跳動。
  • 對於任何由手勢驅動的內容,避免使用 CSS transitions 和 @keyframes——它們無法在飛行途中被順利抓住和反向。彈簧預設從當前數值開始動畫,這正是可中斷性所需要的。
  • 當手勢反向時,融合速度——不要硬切。 在反向點用一個動畫替換另一個會造成速度不連續,就像一堵「磚牆」。能夠在重新設定目標時攜帶速度的彈簧庫可以避免這個問題。(這就是 iOS 的疊加動畫原生的做法;在網頁上,選擇一個在重新設定目標時從當前速度開始的彈簧庫。)
  • 將 2D 運動分解為獨立的 X 和 Y 彈簧。 一個作用於 2D 距離的單一彈簧會在 X 和 Y 速度不同時失調。

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

「把動畫想成是你和物體之間的對話,而不是介面預先寫好的劇本。」

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

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

  • 阻尼比 — 控制超調量。1.0 = 臨界阻尼,沒有彈跳,平穩停止。< 1.0 = 超調並振盪。數值越低越有彈性。
  • 響應時間 — 達到目標值的速度,單位為秒。數值越低越乾脆。這不是「持續時間」——彈簧沒有固定的持續時間;它的穩定時間是從參數中浮現出來的。

預設值:

  • 大多數 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 設為觸發器,這樣按鈕和內容之間的空間關係就很明顯。(這與彈出視窗從其觸發器縮放、而不是從其中心縮放的觀點相同。)
  • 在可逆轉場上鏡像緩動函數,使移出路徑與返回路徑匹配(為兩個方向使用反相的 cubic-bézier 控制點)。

8. 在手勢方向上提供暗示

人類會根據軌跡預測最終狀態。中間的運動應傳達事物將往何處去——控制中心模組會「朝你的手指方向向上向外成長」。讓中間幀指向結果,而不是盲目地進行插值。

9. 橡皮筋效果 — 柔軟邊界

在邊界處,漸進式地抵抗而不是生硬地停止。生硬的停止讀起來像是「凍結」;連續的阻力讀起來像是「有反應,但這裡沒有更多東西了」。應用阻尼,當使用者拖曳越過邊界越遠時,阻尼會增加。

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

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

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

11. 幀級平滑度

平滑度關乎的是幀的內容,而不僅僅是幀率。

  • 保持每幀的位置變化低於感知閾值,以避免閃爍。
  • 對於非常快的運動,微妙的動態模糊/拉伸能編碼速度,看起來比一條尖銳的硬線條更好。
  • requestAnimationFrame 是網頁上與顯示器同步的時脈(Apple 使用 CADisplayLink)。只對合成器友好的屬性進行動畫——transform 和 opacity——並在即將發生運動的地方使用 will-change 提示。

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

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

  • 將導航列/工具列/面板建構為半透明層(backdrop-filter: blur() 加上半透明背景),內容在其下方捲動——而不是佔用固定條塊的不透明長條。
  • 材質重量體現層級: 較暗/較重的材質區分結構區域(側邊欄);較輕的材質將注意力吸引到互動元素(按鈕)。切勿將一個淺色半透明表面疊在另一個上面——可讀性會崩潰。
  • 較大的表面應看起來更厚: 比較小的卡片使用更強的模糊和更深的陰影。考慮情境感知的陰影——在忙碌/文字內容上使用較重的陰影以獲得區隔,在單純背景上使用較輕的陰影。
  • 變暗以聚焦,分離以保持流暢。 一個模態任務會搭配一個變暗的遮罩並將背景推遠/壓低。一個平行、非阻塞的面板則使用半透明和偏移而不使用遮罩,以免打斷流程。對於堆疊的面板,逐層變暗並推遠每個上層。
  • 活力效果能讓文字在變化的背景上保持可讀。 在模糊/半透明表面上,不要使用平坦的灰色文字——使用更高對比度、稍粗的字重和些微的字距增加。將顏色放在實心層上,而不是半透明的前景層。
  • 使用捲動邊緣效果,而不是硬分隔線。 不要在固定頁首下使用 1px 邊框,而是在浮動 Chrome 與內容重疊處淡出一個小的模糊/漸變遮罩——僅限於浮動 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. 和諧 — 視覺、聲音和觸覺必須在同一畫格觸發。它們之間的延遲會破壞幻覺。不要讓一

…

[因長度截斷]

相關技能