动效设计原则 是做什么的?
这是品味层。它决定如何移动,使得动效看起来精致、有意图,而不是卡顿、漂浮或廉价。它不编写执行动效的代码——一旦你决定了曲线、时间和弧线,该决策将成为输入,传递给编排/渲染技能(用于产品视频,传递给 product-demo-director;用于UI,传递给CSS/JS过渡)。请保持这两项工作分离:该技能拥有判断权;编排技能拥有组织和机制权。
触发于任何关于动效感觉的问题:“让它不那么卡顿”、“更流畅”、“更精致”、“更有生命力”、“缓动有问题”、“看起来很廉价”、“我该用ease-in还是ease-out”、“用spring还是插值”。
何时使用同级技能
该技能仅负责曲线/时间/弧线的判断。当真正的问题属于以下情况时,请移交给其他技能:
- 镜头选择、场景顺序或节奏列表 → video-storyboard。
- 编排完整的产品演示——光标移动、屏幕录制、功能标注、端到端场景组装 → product-demo-director。(该技能产生此技能消费的动效规格。)
- 文字或字母动画——飞行/构建/揭示类型的文字 → kinetic-typography。
- 将动效与节拍、配乐或音效同步 → sound-and-music-sync。
- 为平台裁剪或重新构图(9:16, 1:1, 安全区域) → social-video-formatter。
- 光线、曝光、辉光或色彩动效 → motion-color-and-light。
操作流程
先诊断,再开处方。大多数“卡顿”动效属于四种命名问题之一,每种问题都对应特定的修复方法。按顺序执行步骤——如果时间不对,缓动修正就毫无价值;如果元素没有弧线,时间修正也毫无价值。
步骤1:命名问题所在
观察动效(或对其的描述)并归类。几乎所有的抱怨都属于以下之一:
- 线性/机械——匀速移动,然后突然停止。没有缓动。在步骤2中修复。
- 漂浮/绵软——速度太慢,或者同时进行缓入和缓出,本该快入。时长或曲线错误。在步骤2和步骤3中修复。
- 卡顿/抖动——进入和退出速度相同,或者所有属性在同一条平坦时间线上动画,没有偏移。缺少间距和弧线。在步骤3和步骤4中修复。
- 死板/无生命力——技术上正确但无聊:没有预备动作,没有过冲,没有跟随动作。缺少弧线和二次运动。在步骤4和步骤5中修复。
在动手之前,用一行文字写下诊断结果。“模态框因为以ease-in-out在600ms内淡出而显得漂浮”是一个修复方向;“让它好看点”不是。
步骤2:选择缓动曲线
缓动是动效的速度-时间曲线形状。曲线几乎承载了所有感知质量。将曲线与元素的行为相匹配:
- ease-out(快速开始,慢速结束)——元素进入以及用户触发的任何操作的默认值。它感觉响应迅速,因为动效立即开始,然后减速到位。用于:元素出现、下拉菜单打开、点击按钮响应、内容滑入。如果不确定,选择 ease-out。
- ease-in(慢速开始,快速退出)——元素离开。元素加速移出屏幕。如果单独用于进入,会感觉迟钝和滞后,这是“漂浮”感的第一大原因。用于:关闭、退出。
- ease-in-out(慢、快、慢)——将已在屏幕上的元素从A移到B:卡片重新排序、轮播前进、开关滑动。两端都锚定,因此两端缓动感觉可控。不要将其用于进入——慢速启动会破坏响应性。
- spring(物理:质量、刚度、阻尼——过冲然后稳定)——用于动效应感觉有生命力和物理感:模态框弹出、开关旋钮、点赞按钮、通知落下。轻微的过冲正是“有生命力”的原因。调整阻尼以体现个性:较低的阻尼 = 更有弹性和俏皮,较高的阻尼 = 更紧凑和商务。
spring() vs interpolate() 的决策(在代码中何时使用哪一个)位于质量标准和下文可执行工件中。简而言之:spring用于物理/生动的变换,interpolate用于精确到帧的时间值,如透明度淡入淡出、颜色和进度条。
步骤3:设置时间和间距
时间是总时长。间距是距离在时间上的分布方式——间距就是字面意义上的缓动(ease-out意味着帧集中在末尾)。两条规则:
- 时长与距离和尺寸成比例,而非审美。 小元素短距离移动应快速(UI:约150–250ms)。大元素或长距离需要更长的时间(约300–500ms),否则会像瞬移。视频中大的全屏/主要动效可以更长。同样的动效,距离越长→时长越长。
- 进入和退出不对称。 退出应比进入更快(约0.7–0.8倍)——用户已经完成了与该元素的交互,所以让它快速离开。相等的进入/退出时长是“卡顿”的经典来源。
对于产品视频,一旦离开该技能,立即转换为帧数:frames = round(seconds * fps)。这些帧数是您传递给 product-demo-director(它会将这些帧数转换为定时场景计划)的字面输入。
步骤4:构建预期→动作→余韵的弧线
真实的运动很少是单一动作。最丰富、最有“生命力”的动效有三个节拍:
- 预期——在主要动作之前,一个微小的反向蓄力(按钮在弹出前向内轻按,卡片在滑出前向后拉)。它让眼睛做好准备,并使动作显得有意图。保持微小且快速;过多会看起来像故障。
- 动作——主要动作,承载了步骤2的缓动和步骤3的时长。这是间距所在。
- 余韵——元素超过目标然后返回(弹簧过冲),以及任何附加/次级元素稍微滞后于主导元素(标签追上它所位于的卡片)。这就是重量感和生命感的来源。
并非每个微交互都需要三者——一个150ms的工具提示只有动作。但当某些东西感觉“死板”时,几乎总是缺少预期或余韵。在添加装饰之前先添加弧线。
步骤5:应用相关的迪士尼原则
迪士尼的12条动画原则可以直接映射到UI和产品动效上。不需要全部应用;只需使用解决诊断问题的少数几个。完整映射在 references/twelve-principles 中。高杠杆率的原则:
- 慢入慢出 → 缓动(步骤2)。单个最大的质量杠杆。
- 预期 → 蓄力(步骤4)。
- 余韵与重叠动作 → 过冲 + 滞后的次要元素(步骤4)。“正确”与“有生命力”之间的区别。
- 时间 → 时长和间距(步骤3)。
- 挤压与拉伸 → 在按压/碰撞时微妙的缩放以暗示质量;在UI中保持在几个百分点以内,否则会看起来像玩具。
- 表演 → 一次只移动一个东西,让眼睛知道看哪里;绝不同时在相同曲线上动画化六个东西。
- 辅助动作 → 一个小的支持性动效(卡片抬起时阴影加深),强化主要动作而不与之竞争。
- 夸张 → 将关键姿势略微推过现实;在UI中克制,在有力的产品视频中更自由。
步骤6:移交规格
填写下面的动效规格模板——曲线、时长(和帧数)、弧线节拍以及每个属性的spring-vs-interpolate选择。填写好的规格是交接工件:对于产品视频场景,它直接输入到 product-demo-director;对于UI,它成为您的过渡/关键帧值。如果场景还需要光线/曝光动效,请与 motion-color-and-light 配合使用;如果文字正在动画化,请与 kinetic-typography 配合使用;如果时间必须落在节拍上,请与 sound-and-music-sync 配合使用。
质量标准
动效只有满足以下所有条件才能达到A+:
- 诊断已命名(线性/漂浮/卡顿/死板),并且每个更改都可追溯到此诊断。没有无理由的“让它好看点”。
- 进入使用ease-out,退出使用ease-in,屏幕上A→B使用ease-in-out,生动/物理使用spring。 除非是故意的匀速循环(如加载旋转器、跑马灯),否则不允许原始线性动效。
- 时长与距离/尺寸成比例,且退出快于进入。
spring()与interpolate()的选择针对每个属性显式声明:物理变换用spring(缩放、平移、弹出/滑动/稳定——需要过冲);精确时间值用interpolate(透明度淡入淡出、颜色、进度,任何过冲看起来不对的地方)。由spring驱动的淡入淡出,或由线性interpolate驱动的标志弹出,都是缺陷。- 在主要/标志性动效上至少存在预期或余韵之一;无生命力的动效正是缺少这些。
- 一次只主导一个东西(表演);属性应偏移而非全部在同一条平坦时间线上触发。
- 曲线、时长和帧数已书面记录,并准备好移交给product-demo-director或过渡——该技能绝不将决策隐式化。
禁止事项
- 不要为进入或退出使用线性缓动——这是机械的默认值,看起来未完成。匀速仅用于有意的循环。
- 不要为进入使用ease-in,或为退出使用ease-out——这是相反方向上的漂浮/滞后感。进入使用ease-out,退出使用ease-in。
- 不要使用相等的进入和退出时长;退出应快约0.7–0.8倍。
- 不要用spring驱动淡入淡出、颜色或进度条——过冲会使值超出范围并闪烁。时间值请使用interpolate。
- 不要在你希望动效感觉有生命力时,用线性interpolate驱动生动的弹出/滑动/缩放——使用spring来获得过冲。
- 不要在同一条平坦时间线上动画化多个属性;应分阶段并偏移它们。
- 不要在UI中过度预期或过度过冲——几个像素/几个百分点即可。大幅的蓄力和弹跳会看起来像故障或玩具。
- 不要在此处规划镜头顺序或组装演示——将规格移交给product-demo-director;故事板顺序由video-storyboard负责。
缓动与spring选择器
自包含的Node脚本。保存为 motion_pick.js 并使用 node motion_pick.js 运行。编辑 requests 数组以包含您正在动画化的元素;它将返回曲线、按距离缩放的时长(毫秒和帧数)、退出时长以及spring-vs-interpolate选择。无依赖。
// 动效选择器。编辑 `requests`,然后:node motion_pick.js
const fps = 30 // 用于产品视频;UI忽略帧列
// role: 'enter' | 'exit' | 'move' | 'lively'
// distancePx: 移动距离(驱动时长)。sizePx: 元素大致尺寸。
const requests = [
{ name: 'Modal pop-in', role: 'lively', property: 'scale', distancePx: 0, sizePx: 600 },
{ name: 'Modal backdrop', role: 'enter', property: 'opacity', distancePx: 0, sizePx: 1920 },
{ name: 'Toast slide-in', role: 'enter', property: 'translate', distancePx: 320, sizePx: 360 },
{ name: 'Toast dismiss', role: 'exit', property: 'translate', distancePx: 320, sizePx: 360 },
{ name: 'Card reorder', role: 'move', property: 'translate', distancePx: 140, sizePx: 280 },
{ name: 'Progress fill', role: 'move', property: 'width', distancePx: 800, sizePx: 800 },
]
function curveFor(role) {
if (role === 'enter') return 'ease-out'
if (role === 'exit') return 'ease-in'
if (role === 'move') return 'ease-in-out'
return 'spring' // lively
}
// 时长随移动距离和元素尺寸缩放。基线+斜率,带钳位。
function durationMs(role, distancePx, sizePx) {
const reach = Math.max(distancePx, sizePx * 0.4)
let ms = 140 + reach * 0.45
ms = Math.min(Math.max(ms, 140), 600)
if (role === 'exit') ms *= 0.75 // 退出更快
return Math.round(ms)
}
// spring用于物理变换;interpolate用于时间值。
function driver(role, property) {
const physical = property === 'scale' || property === 'translate'
if (role === 'lively') return 'spring() // 需要过冲'
if (physical && role !== 'move') return 'spring() // 物理,生动的稳定'
return 'interpolate() // 精确,同步到帧'
}
const pad = (s, n) => String(s).padEnd(n)
console.log(pad('ELEMENT', 18), pad('CURVE', 12), pad('MS', 6), pad('FRAMES', 8), 'DRIVER')
for (const r of requests) {
const curve = curveFor(r.role)
const ms = durationMs(r.role, r.distancePx, r.sizePx)
const frames = Math.round((ms / 1000) * fps)
console.log(pad(r.name, 18), pad(curve, 12), pad(ms, 6), pad(frames, 8), driver(r.role, r.property))
}
示例输出
使用上述请求,脚本打印:
ELEMENT CURVE MS FRAMES DRIVER
Modal pop-in spring 252 8 spring() // 需要过冲
Modal backdrop ease-out 600 18 interpolate() // 精确,同步到帧
Toast slide-in ease-out 284 9 spring() // 物理,生动的稳定
Toast dismiss ease-in 213 6 interpolate() // 精确,同步到帧
Card reorder ease-in-out 203 6 interpolate() // 精确,同步到帧
Progress fill ease-in-out 500 15 interpolate() // 精确,同步到帧
解读:模态框弹出使用spring(过冲=有生命力),而其背景仅淡入使用定时interpolate——同一组件上两种不同的驱动方式,完全符合质量标准的要求。Toast在284ms内进入,在213ms内更快地退出(0.75倍退出规则),其滑动是物理的,因此用spring进入但用interpolate退出。进度条是精确的宽度值——绝不使用spring,否则填充会抖动。