【免费下载链接】open-slide
A slide framework built for agents.
本篇指南以
.agents/skills/apple-design/SKILL.md为骨架,将 Apple 的界面设计哲学与流体物理动效(源自 WWDC 2018《Designing Fluid Interfaces》)翻译为 Web 平台的实战准则:涵盖响应式反馈、1:1 直接操控、可打断的弹簧动画、速度交接、动量投影、橡皮筋边界、材质与层级、动效无障碍与字体排印。读者将掌握一套可直接用于 Open Slide 幻灯片与 Web UI 的「设计→动效」决策体系,并看到它在仓库核心实现中的真实落点。
为什么这套准则值得照做
Apple 的界面之所以「不像电脑、像身体的一部分」,是因为它遵循一条主线:界面有生命感的时刻,是动效从屏幕当前值出发、继承用户的速度、向前投射动量,并且能在任意瞬间被抓住并反向操作。弹簧(spring)是实现这一切的自然工具——它天然可打断、且感知速度。
从原理层面看,界面的「流畅」本质上就是模拟物理世界:即时响应、连续运动、携带动量、在边界处抵抗、可在运动中途改道。Apple 把设计视为服务人类的四种需求——安全/可预测、理解、成就感、愉悦——下方每一条规则都服务于其中之一。
在 Open Slide 仓库中,这套理念并非纸上谈兵。幻灯片翻页过渡层(slide-transition-layer.tsx)会在索引变化时先取消所有在途动画再启动新过渡(对应「可打断」原则);Step步骤组件通过prefers-reduced-motion把过渡时长直接归零(step-context.tsx);激光笔跟随光标时绕过 React 状态、直接在requestAnimationFrame内写 transform(laser-pointer.tsx)。这些代码都可以作为下文的佐证。
1. 响应(Response)——消灭延迟
一旦出现滞后,操控的「直接感」就会断崖式消失。响应是一切的地基:
- 在 pointer-down 时响应,而不是等松手。按钮被按下的瞬间就要高亮;等到
click/touch-up 才反馈,界面就显得死气沉沉。 - 警惕每一处延迟。审查 debounce、人为定时器、过渡等待,以及约 300ms 的点击延迟。输入路径上任何非必要的环节都是退化。
- 反馈必须贯穿交互过程,而非只在结束时出现。拖拽、滑块、抽屉在全程都要与指针 1:1 更新,绝不要只在手势完成时才动一下。
/* 反馈发生在按压瞬间,且即时生效 */ .button:active { transform: scale(0.97); transition: transform 100ms ease-out; }在 Open Slide 的演示播放器中,激光笔组件正是「响应即时化」的典型实现:它在pointermove时把坐标写入 ref,然后通过requestAnimationFrame直接设置元素的transform,注释明确说明「经 React state 路由会在指针速率下重渲染、拖慢跟手」——laser-pointer.tsx。
2. 直接操控(Direct Manipulation)——1:1 跟手
「触摸与内容应该一起移动。」
用户拖拽某物时,它必须紧贴手指——并且尊重用户抓取时的偏移。抓取时把元素中心吸附到指针会立刻打破幻觉:
- 使用 Pointer Events +
setPointerCapture,这样指针离开元素边界后跟踪依然持续。 - 记录一段简短的速度/位置历史(最近几次
pointermove),而不只是当前点——松手瞬间你需要速度。
el.addEventListener('pointerdown', (e) => { el.setPointerCapture(e.pointerId); const grabOffset = e.clientY - el.getBoundingClientRect().top; // 尊重抓取位置 // ...同时记录位置 + 时间戳历史,用于计算速度 });3. 可打断性(Interruptibility)——最重要的单一原则
「想法与手势是并行发生的。」
任何动画都必须能在任意时刻被打断、被重新定向。用户应该能抓住一个正在运动的元素中途掉头,而不必等动画播完;一个正在关闭的弹窗如果再次被抓住,应该跟随手指——而不是先关完再重新打开。
- 过渡期间绝不锁死输入。
- 永远从「当前呈现」值(presentation value)起步,而不是目标值。被打断时,读取元素在屏幕上的实时 transform,从那里启动新动画。从逻辑/目标值起步会产生可见的跳动。
- 手势驱动的动效避免使用 CSS transition 与
@keyframes——它们无法在途中被平滑抓住并反向。弹簧默认从当前值起步,这正是打断所需要的。 - 手势反向时混合速度,不要硬切。在反向点用新动画替换旧动画会产生速度不连续,像「撞墙」。能跨重定向携带速度的弹簧库可以避免这一点(这正是 iOS 原生additive animations所做的;在 Web 上,选择能从当前速度重定向的弹簧库)。
- 把二维运动拆成独立的 X/Y 弹簧。当 X、Y 速度不同时,作用在 2D 距离上的单一弹簧会失步。
在 Open Slide 中,SlideTransitionLayer的效果钩子会在每次索引变化时先for (const a of animsRef.current) a.cancel()取消在途动画,随后才解析并执行新过渡——这正是「可打断、可重定向」原则的工程化体现,见 slide-transition-layer.tsx。
4. 行为优先于动画——使用弹簧
「把动画想成你与对象之间的对话,而不是界面规定好的脚本。」
预设的固定时长动画无法响应新输入;弹簧可以——新输入只是改变目标,运动保持连续。凡是用户能碰到的交互,都用弹簧。
Apple 有意用两个设计师友好的参数取代物理三元组(质量/刚度/阻尼):
- 阻尼比(Damping ratio)——控制过冲。
1.0= 临界阻尼,无弹跳、平顺收敛;< 1.0= 过冲并振荡,越小越弹。 - 响应(Response)——值到达目标的速度,单位秒。越小越快。这不是「时长」——弹簧没有固定时长,收敛时间由参数涌现而来。
默认值建议:
- 大多数 UI 从阻尼
1.0(临界阻尼)起步——优雅且不分散注意力。 - 只有当手势本身携带动量时(轻扫、甩出、拖拽松手)才加入弹跳(阻尼约
0.8)。一个淡入菜单的过冲感觉很怪;一个被你甩出去的卡片过冲则恰到好处。
Apple 实际发布的取值:
| 交互 | 阻尼 | 响应 |
|---|---|---|
| 移动/重定位(如画中画) | 1.0 | 0.4 |
| 旋转 | 0.8 | 0.4 |
| 抽屉/面板 | 0.8 | 0.3 |
Web 映射(Motion / Framer Motion):bounce+duration弹簧 API 与 Apple 的 damping + response 高度对应。安全的内置风格是默认全用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. 速度交接(Velocity Handoff)——拖拽与动画之间的接缝
手势结束时,动画必须以手指的精确速度继续,这样拖拽与动画之间没有任何可见接缝。这是区分「流畅」与「不错」最关键的细节。
把指针的松手速度作为弹簧的初始速度传入。有些弹簧 API 需要相对速度——按到达目标的剩余距离归一化:
relativeVelocity = gestureVelocity / (targetValue − currentValue)示例:元素在y=50,目标y=150(还有 100px),手指速度 50px/s → 弹簧初始速度 =50 / 100 = 0.5。Framer Motion / Motion 直接接受绝对 px/s 速度(velocity选项),通常直接把原始值交给它。
6. 动量投影(Momentum Projection)——动画到手势「要去的地方」
「用小输入,放大输出。」
不要从松手点吸附到最近边界。要用速度投影静止位置——就像滚动减速那样——再吸附到离投影点最近的目标。这正是轻扫「把元素甩出去」的感觉来源。
Apple 的精确投影函数(来自《Designing Fluid Interfaces》示例代码):
// 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 用的形式——请使用上面的指数衰减形式。这也是优秀 bottom-sheet 与轮播组件(Vaul、Embla)的标准行为。
7. 空间一致性(Spatial Consistency)——对称路径、锚定原点
「如果某物以某种方式消失,我们期待它从来的地方出现。」
- 进出沿同一条路径。从右侧滑入的面板必须从右侧退出。右进/底出会显得割裂、令人困惑。
- 交互锚定到它的来源。菜单、popover、sheet 应从触发它的元素处起源——把
transform-origin设为触发器,让按钮与内容之间的空间关系一目了然。 - 可逆过渡镜像缓动。让出向路径与返回路径一致(两个方向使用相反的 cubic-bézier 控制点)。
8. 沿手势方向做暗示(Hint in the direction of the gesture)
人类会从轨迹预测最终状态。中间帧应该「预告」去向——Control Center 的模块会「朝你的手指向上、向外生长」。让中间帧指向结果,而不是盲目地向结果插值。
9. 橡皮筋(Rubber-banding)——软边界
到达边界时,渐进抵抗而不是硬停。硬停读作「冻结」;持续抵抗读作「有响应,但这里没有更多了」。对越拖越远的边界施加递增阻尼:
// 越过边界越多,元素跟随越少——真实物体减速后再停下 function rubberband(overshoot, dimension, constant = 0.55) { return (overshoot * dimension * constant) / (dimension + constant * Math.abs(overshoot)); }10. 手势设计细节(「手感」检查清单)
- 点击:touch-down高亮(即时)、touch-up提交。为目标周围增加约 10px 的滞回/命中内边距,并允许「拖走取消」与「拖回恢复」。
- 拖拽/滑动:在确定方向前需要一个小的移动阈值(滞回,约 10px),然后 1:1 跟手。
- 从第一次移动起并行检测所有可能手势,一旦意图明确就果断取消失败者。避免只报告最终状态的识别器(如
swipeleft类事件)——它们丢弃了反馈所需的连续跟踪。 - 尽量减少消歧延迟。双击检测不可避免地延迟单击;只在确实存在双击的地方付出这个代价。
11. 帧级平滑(Frame-level smoothness)
平滑关乎帧里有什么,而不只是帧率。
- 把每帧的位置变化控制在感知阈值以下,避免闪烁(strobing)。
- 极快运动时,轻微的运动模糊/拉伸能编码速度,比尖锐的拖影读起来更好。
requestAnimationFrame是 Web 的显示同步时钟(Apple 用CADisplayLink)。只动画合成器友好的属性——transform与opacity——并在运动即将发生时用will-change提示。
Open Slide 的激光笔正是此节标准的执行范例:18px 的圆点用willChange: 'transform'声明,位置在 rAF 回调中直接写入translate3d,见 laser-pointer.tsx。
12. 材质与纵深(Materials & depth)——半透明传达层级
Apple 用半透明材质作为漂浮的功能层,带来结构而不抢焦点。Web 上用backdrop-filter近似:
- 把导航栏/工具栏/面板做成半透明层(
backdrop-filter: blur()+ 半透明背景),内容在下面滚动——而不是占据固定一条的不透明条。 - 材质重量编码层级:更深/更重的材质区隔结构区域(侧栏);更轻的材质把注意力引向交互元素(按钮)。绝不在另一个浅色半透明表面上叠加浅色半透明表面——可读性会崩塌。
- 更大的表面读作更厚:比小卡片更强的模糊 + 更深的阴影。考虑上下文感知阴影——在繁忙/文本内容上更重以区隔,在纯色背景上更轻。
- 变暗以聚焦,分离以保流畅。模态任务用表面 + 变暗遮罩(scrim)并把背景压下去。并行的非阻塞面板用半透明与偏移而不用遮罩,以免打断流动。多层 sheet 逐层变暗并压回每个父层。
- Vibrancy 让文字在变化背景上保持可读。在模糊/半透明表面之上,不要用平坦灰字——用更高对比度、稍重字重,并小幅增加字距。把颜色放在实色层,而不是半透明前景上。
- 滚动边缘效果,而非硬分隔线。吸顶 header 下不要用 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. 多模态反馈(Multimodal feedback)——动效 + 声音 + 触觉
结合多种感官的三条规则(源自《Designing Audio-Haptic Experiences》):
- 因果性(Causality)——反馈的起因必须显而易见。在实际因果事件上触发(开关翻转、元素归位),并让它的特征匹配动作的物理性。
- 和谐(Harmony)——视觉、声音、触觉必须在同一帧触发。它们之间的延迟会摧毁幻觉。不要让 CSS 过渡拖慢音频/触觉(Vibration API)。
- 效用(Utility)——只在值得的地方添加反馈。把触觉/声音留给有意义的时刻(成功、错误、提交、吸附)。过度反馈会训练用户忽略一切反馈。
14. 减少动效与无障碍(Reduced motion & accessibility)
减少动效不等于没有反馈——它意味着更温和、不诱发前庭眩晕的等价物。响应三个独立信号,并把它们烘焙进组件:
prefers-reduced-motion: reduce——把滑入/弹簧/视差换成短促的透明度交叉淡化或静态过渡。去掉弹性/过冲。保留有助于理解的透明度/颜色变化。prefers-reduced-transparency: reduce——让半透明表面更「霜化」/实心:提高背景不透明度,去掉模糊。prefers-contrast: more——近实心背景 + 明确的对比边框。
另外:避免全视口运动背景、慢速循环振荡(接近 0.2 Hz / 每 5 秒一个周期)、突然的亮度跳变(暗↔亮主题切换要缓动)。大型移动物体移动时保持半透明,大幅重定位后淡出大表面、稳定后再淡入。
@media (prefers-reduced-motion: reduce) { .sheet { transition: opacity 200ms ease; transform: none !important; } } @media (prefers-reduced-transparency: reduce) { .toolbar { background: white; backdrop-filter: none; } }在 Open Slide 源码中,这一节被完整落地:
- use-prefers-reduced-motion.ts 把
(prefers-reduced-motion: reduce)封装成 React Hook 与同步查询函数; Step组件读取该 Hook,在开启减少动效时把步骤过渡时长设为 0ms——const ms = reduceMotion ? 0 : duration,见 step-context.tsx;- styles.css 对入场动画
rise-in、评论提示光圈、行加载条等均提供了prefers-reduced-motion: reduce下的「动画关闭」降级。
15. 字体排印(Typography)——光学尺寸、字距、行高
Apple 让字体随字号改变形态;同样的纪律适用于 Web。(源自 WWDC 2020《The Details of UI Typography》。)
- 字距(tracking/letter-spacing)随字号变化——永远不要一个值打天下。大号展示文字需要负字距(字长大后间距显得太开);小号文字需要轻微正字距保证可读。固定的
letter-spacing总在某个字号上是错的。标题收紧,正文接近0。 - 行高(leading)随字号反向变化。大标题紧,正文松。对高升部/降部的字体放宽;对密集信息型 UI 收紧。
- 用「字重 + 字号 + 行高」成组建立层级,而不是只靠字号。用字重强调——它不占额外空间就增加存在感。
- 尊重用户的文字大小设置(Dynamic Type)。布局随文字缩放——间距用
rem/em而非固定 px——这样更大的字体不会撑坏布局。 - 自定义字体之前默认使用平台系统字体;它自带光学尺寸、字距表与可读性调优。只有有理由时才覆盖。
:root { font: 100%/1.5 system-ui, sans-serif; } /* 正文:系统字体,舒适行高 */ .display { font-size: clamp(2rem, 5vw, 4rem); line-height: 1.05; /* 大字号紧凑行高 */ letter-spacing: -0.02em; /* 增大时负字距 */ font-optical-sizing: auto; }16. 设计基础——八原则
上面的动效与工艺服务于 Apple 的八条设计原则(Principles of Great Design, WWDC 2026)。用这些名字作为你推理的依据:
- 目的(Purpose)。带着意图去构建;决定不做什么。每个功能都在索取用户的时间、注意力与信任——只在有回报的地方花这笔预算。
- 自主(Agency)。让人保持掌控:提供选择,不强迫单一路径。用宽恕兜底——轻松撤销失误;只有真正破坏性、不可逆的动作才用确认对话框(克制使用;滥用会训练人们点击跳过)。
- 责任(Responsibility)。为用户利益行事。隐私:在合适时机、只问必要信息、透明地询问。安全:预判滥用与伤害——尤其涉及 AI 时(一个懂过敏原的食谱应用绝不能推荐有害食材)。提供预览、确认、免责声明;砍掉风险大于价值的特性。
- 熟悉(Familiarity)。建立在人们已知的事物上。使用既不太具体也不太抽象的隐喻(垃圾桶代表删除),尊重它们的物理。保持一致:看起来相同的事物行为必须相同、位置必须相同(macOS 上关闭永远在左上角),这样人们能预测接下来发生什么。只有在能证明更好时才打破熟悉模式——然后去测试它,不要想当然。
- 灵活(Flexibility)。为不同上下文、设备与完整能力范围而设计。适配平台(iPhone = 快速触摸;桌面 = 精确指针控制的深度工作流)与情境。包容性设计(年龄、语言、专长、无障碍)。当单一布局无法适配所有人时,允许个性化——重排控件、隐藏不用的部分。
- 简洁——不是极简(Simplicity, not minimalism)。去掉多余,让核心目的闪耀;把一切埋在一处看起来极简但不简单。要简洁(平实语言、无行话、少步骤)、要清晰(用层级——顺序、间距、对比——让最重要的事最显眼)。每个元素都要挣得位置;有时添加上下文反而简化(显示剩余时间的视频进度条)。先展示常见路径,高级选项深一层。
- 工艺(Craft)。不妥协的细节建立信任。优美的字体、适配亮/暗的颜色、清晰的图标、即时自然反馈的响应式动画。没有什么是随机的——每个间距、时序、对齐值都是你能捍卫的深思熟虑的选择。抖动滚动、错位图标、旋转后崩坏的布局读作粗心。工艺需要迭代与长寿——随功能与硬件演进持续打磨设计。
- 愉悦(Delight)。是把前七条做对的结果,不是事后贴的彩纸。决定你想让人感受的情绪(平静、自信、兴奋),并在每个决策中强化它。
服务这些原则的战术规则:
- 反馈有四种:状态、完成、警告、错误。确认有意义的动作、暴露进行中状态、问题发生前警告、内联校验(而不是提交时)。
- 寻路(Wayfinding)。每个屏幕都应回答:我在哪?我能去哪?那里有什么?我如何出去?绝不困住用户。
- 分组与映射(Grouping & mapping)。邻近意味着关系;把控件放在它所影响事物旁边,让控件排列镜像它们改变的事物。如果需要标签来解释一个控件,说明映射很弱。
- 直接、具体的标签胜过安全的泛称。用内容命名导航项(「进度」「资料库」),而不是模糊的大伞(「首页」)。具体创造可预测性。
17. 流程(Process)
- 交互式原型——一个交互演示胜过「一百万张静态设计图」。你通过构建与把玩来发现界面;可运行的原型也设定了具体标准,防止最终实现平庸。
- 交互与视觉一起设计。「你不应能说出一个在哪结束、另一个在哪开始。」动效不是加在像素之后的图层。
- 在真实语境中用真人测试,并以新鲜眼光审视动效——慢放/逐帧播放,捕捉全速下看不见的问题。
快速参考表
| 需求 | 技术 | 具体取值 |
|---|---|---|
| 默认 UI 弹簧 | 临界阻尼、无过冲 | damping 1.0,response 0.3–0.4 |
| 动量/轻扫弹簧 | 欠阻尼、轻微弹跳 | damping ~0.8,response 0.3–0.4 |
| 手势 → 弹簧速度 | 交接松手速度 | 归一化时gestureVelocity / (target − current) |
| 轻扫落点 | 投影动量 | current + (v/1000)·d/(1−d),d ≈ 0.998 |
| 干净打断 | 从呈现(实时)值起步 | 读取屏幕上的 transform |
| 避免反向「撞墙」 | 跨重定向携带速度 | 会混合速度的弹簧 |
| 可逆过渡 | 镜像缓动曲线 | 反向 cubic-bézier |
| 判断反向还是提交 | 用速度符号而非位置 | 松手时判定 |
| 1:1 拖拽 | Pointer Events + capture | 尊重抓取偏移 |
| 反馈 | 在 pointer-down、持续 | 绝不只在结束时 |
| 边界 | 橡皮筋而非硬停 | 渐进抵抗 |
| 半透明 chrome | backdrop-filter层 | 内容在下方滚动 |
| 字体字距 | 随字号、绝不固定 | 大字收紧(-0.02em),正文接近0 |
| 减少动效 | 交叉淡化而非滑入/弹簧 | @media (prefers-reduced-motion) |
如何在 Open Slide 中应用这套体系
把这份技能清单落到 Open Slide 幻灯片与演示引擎时,可以从三个层次入手:
- 组件层:为可交互元素(按钮、抽屉、缩略图栏、命令面板)采用「默认临界阻尼 + 动量场景才弹跳」的弹簧策略,并保证所有动画从当前呈现值起步、可随时打断。Open Slide 的
SlideTransitionLayer已经在索引切换时执行「先取消、再过渡」,你的自定义过渡与步骤动画应保持同样的可打断语义,见 slide-transition-layer.tsx 与 transition.ts。 - 系统层:把
prefers-reduced-motion、prefers-reduced-transparency降级内置到你的组件里——直接复用 use-prefers-reduced-motion.ts 的封装模式即可;步骤动画的 0ms 降级是一个现成范例。 - 内容层:幻灯片内的元素进场使用基于 transform/opacity 的组合器友好动画,正文保持舒适行高与字号相关字距,减少动效模式下退化为交叉淡化。
这套准则的全部细节都沉淀在 .agents/skills/apple-design/SKILL.md,可作为构建或评审任何手势驱动 UI 时的对照清单;Open Slide 仓库中的对应实现(slide-transition-layer.tsx、step-context.tsx、laser-pointer.tsx、styles.css)则提供了可直接研读的落地样本。
【免费下载链接】open-slide
A slide framework built for agents.
相关推荐
Web 动画设计实战指南:从微交互细节到 Typebot 组件库的动画落地实践
Web 动画设计实战指南:从微交互细节到 Typebot 组件库的动画落地实践 本文源自 Typebot 开源仓库中 .agents/skills/web an
前端后端低代码AI 应用SwiftUI 弹簧动画终极指南:15个实用效果打造生动交互体验
SwiftUI 弹簧动画终极指南:15个实用效果打造生动交互体验 想要为你的 iOS 应用添加令人愉悦的物理交互效果吗?OpenSwiftUIAnimation
示例工程前端react-bits apple-design 技能文档:把苹果「流体界面」设计哲学落地为 Web 动效工程实践
react bits apple design 技能文档:把苹果「流体界面」设计哲学落地为 Web 动效工程实践 本文以 react bits 仓库中 AGEN
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考