Every figure on this page is operable — drag it, flick it, interrupt it mid-flight. The system behind them is two layers with one hard boundary: page motion uses easing curves and never bounces; springs live only inside gesture-driven components. Zero dependencies, from WWDC's fluid-interfaces playbook. 这一页的每张图都能上手——拖它、甩它、飞行途中抓住它。背后是带一条硬边界的两层体系:页面动效只用缓动曲线、永不弹跳;弹簧只住在手势驱动的组件里。零依赖,源自 WWDC 流体界面方法。
Entrances, hovers, presses. Everything here runs on CSS easing tokens — no springs, no physics, no surprises. The craft is in latency: feedback lands on pointer-down, and panels leave the way they came. 入场、悬停、按压。这一层全部跑在 CSS 缓动 token 上——没有弹簧、没有物理、没有意外。工夫在延迟上:反馈落在按下的瞬间,面板从哪来回哪去。
Press and hold each button. The left one responds the instant your finger lands; the right one waits for release plus 220ms — the moment lag appears, directness falls off a cliff. 按住两个按钮各试一次。左边在手指落下的瞬间就有反应;右边等你松手再等 220ms——延迟一出现,"直接操纵"的感觉立刻消失。
Feedback on pointer-down. 100ms scale, nothing waits.反馈在 pointer-down。100ms 缩放,不等任何事。
Feedback only after release, artificially delayed. This is what debounces and tap delays feel like.反馈只在松手后,还被人为延迟。防抖和 tap delay 就是这种手感。
.apple-button:active {
transform: scale(0.97); /* lands the moment the pointer does */
transition: transform 100ms var(--ease-apple-out);
}
The standard apple-design entrance: 700ms ease-out, 80ms sibling stagger, opacity plus 24px of travel. Replay it — notice it settles without overshoot. 标准的 apple-design 入场:700ms ease-out、兄弟元素错峰 80ms、透明度 + 24px 位移。重放一次——注意它落定时没有过冲。
One easing pair, three durations.一对缓动,三档时长。
80ms between siblings, ≤5 per group.兄弟间 80ms,每组 ≤5。
2–3 animated nodes per page.每页 2–3 个动画节点。
e.target.style.transition =
`opacity 700ms cubic-bezier(0.25,1,0.5,1) ${i * 80}ms,
transform 700ms cubic-bezier(0.25,1,0.5,1) ${i * 80}ms`; /* IntersectionObserver entrance */
A panel that slides in from the right must dismiss to the right. Play both versions — the broken one exits downward, and the room stops making sense. 从右侧滑入的面板必须向右退出。两个版本各播一次——坏的那个向下退场,空间关系立刻断裂。
The panel anchors to the right edge — its home. Exit through the floor and it no longer has one.面板锚定在右缘——那是它的家。从地板退场,它就没有家了。
/* in from the right … */ transform: translateX(0); /* … out to the right */ transform: translateX(calc(100% + 40px)); /* mirror the easing so the return path matches the way in */
transform-origin for the same reason.
规则:东西从哪个方向消失,就该从哪里回来。popover 用 transform-origin 锚定触发元素,也是同一个道理。
Everything below tracks your pointer 1:1, hands your velocity to a spring on release, and can be grabbed back mid-flight. This is where fluid comes from. 下面每个例子都 1:1 跟着指针走,松手时把你的速度交给弹簧,飞行途中随时能被抓回来。"流畅"就是从这里来的。
Click anywhere in the strip and the ball springs there. Click again before it settles: the new flight starts from the current position and the current velocity — no jump, no brick wall. Then try Apple's shipped parameter pairs. 在条带里任意位置点击,小球弹簧过去。落定前再点一次:新的飞行从当前位置和当前速度出发——不跳变、不撞墙。然后试试 Apple 出货的参数组。
const w0 = (2 * Math.PI) / response; // Apple's two designer params v += (-w0*w0*(x - target) - 2*damping*w0*v) * dt; // semi-implicit Euler x += v * dt; // retarget() = interruption
Drag the row and let go with a flick. The release velocity projects a landing point — (v/1000)·d/(1−d) — and the track snaps to the card nearest that projection, not nearest your finger. The blue index is the chosen target.
拖动卡片行,然后甩手放开。松手速度投射出滑行落点——(v/1000)·d/(1−d)——轨道吸附到离落点最近的卡片,而不是离手指最近的。蓝色编号就是被选中的目标。
const landing = current + project(releaseVelocity); // where the flick is going const target = nearestSnap(landing); // choose from the projection spring(current, target, releaseVelocity, apply); // then hand off the velocity
Drag the knob past either end of the groove. The thin gray line is your actual pointer; the knob falls progressively behind it — resistance that grows with overshoot. Release and it springs home with your velocity. 把旋钮拖出凹槽两端。细灰线是你指针的真实位置;旋钮渐渐落在它后面——阻力随越界量增长。松手后带着你的速度弹回边界。
function rubberband(overshoot, dimension, c = 0.55) {
return (overshoot * dimension * c) / (dimension + c * Math.abs(overshoot));
} /* a hard stop reads as “frozen”; growing resistance reads as “nothing more here” */
The classic bottom sheet, with Apple's shipped drawer parameters. Two things to try, described on the right. 经典的底部抽屉,用的是 Apple 抽屉出货参数。右边写了两件值得一试的事。
Drag the sheet. Release past halfway while flicking downward and it still closes — the release decision reads the sign of your velocity, not where the sheet happens to be.拖这张抽屉。拖过一半但往下甩着松手,它照样关闭——松手判定读的是速度的方向,不是抽屉恰好在哪。
Grab it mid-flight. While it glides, catch it again: it follows from its current on-screen position. Apple ships this pair as damping 0.8, response 0.3.飞行途中抓住它。滑行时再抓一次:它从当前屏上位置继续跟手。Apple 给抽屉出货的参数是阻尼 0.8、响应 0.3。
const goOpen = Math.abs(v) > 150 ? v < 0 // fast: the flick direction wins
: y < travel / 2; // slow: nearest detent
spring(y, goOpen ? OPEN : PEEK, v, apply, { damping: 0.8, response: 0.3 });
| Interaction交互 | damping | response | Why为什么 |
|---|---|---|---|
| Move / reposition移动 / 归位 | 1.0 | 0.4 | no momentum in the gesture → no overshoot手势不带动量 → 不许过冲 |
| Rotation旋转 | 0.8 | 0.4 | spin carries momentum → a little bounce旋转带动量 → 一点回弹 |
| Drawer / sheet抽屉 / sheet | 0.8 | 0.3 | flicked shut → snappier, slight give被甩上 → 更快落定,略有余韵 |
CSS is handled globally by apple.css. But rAF springs are JavaScript — the stylesheet cannot stop them. Every spring call on this page checks the same gate first. Flip the switch: every demo above obeys it, live. CSS 由 apple.css 全局兜底。但 rAF 弹簧是 JavaScript——样式表拦不住它。这一页每次弹簧调用都先过同一道门。拨动开关:上面所有 demo 立刻全部服从。
prefers-reduced-motion: reduce模拟 prefers-reduced-motion: reduce
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) { jumpTo(target); fadeIn(120); } // no glide, keep the outcome
else { spring(current, target, velocity, apply); }