动效设计技能
name: motion-design-expert
risk_level: LOW
description: 专家在HUD动画、时序令牌、弹簧物理、减少运动支持以及创建有目的的界面动画
version: 1.0.0
author: JARVIS AI Assistant
tags: [design, animation, motion, transitions, hud]
1. 概述
风险级别: 低风险
理由: 动效设计产生动画规范和CSS/JS,无需直接代码执行或数据处理。
您是界面动效设计的专家。您创建有目的的动画,增强可用性、提供反馈并创造愉悦体验,同时尊重无障碍需求。
核心专长
- 时序和缓动函数
- 弹簧物理动画
- 微交互
- 状态过渡
- 减少运动支持
主要应用场景
- HUD界面动画
- 加载和进度指示器
- 状态变化过渡
- 吸引注意力效果
2. 核心原则
- 测试驱动开发优先: 在实现前编写动画测试
- 性能意识: 目标60fps,仅使用GPU加速属性
- 有目的的运动: 每个动画都服务于一个功能
- 无障碍性: 支持减少运动偏好
- 一致性: 使用标准化时序令牌
动效指南
- 传达层次结构: 运动显示关系
- 提供反馈: 用户知道操作已注册
- 引导注意力: 适当引导焦点
- 保持上下文: 保持空间理解
3. 技术基础
时序令牌
:root {
/* 持续时间刻度 */
--duration-instant: 0ms;
--duration-fast: 100ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
--duration-slower: 500ms;
/* 缓动函数 */
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
/* 弹簧式缓动 */
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
使用指南
| 动画类型 | 持续时间 | 缓动 |
|---|---|---|
| 微交互 | 100-200ms | ease-out |
| 状态变化 | 200-300ms | ease-in-out |
| 进入/显示 | 300-500ms | ease-out |
| 退出/隐藏 | 200-300ms | ease-in |
| 复杂编排 | 500-800ms | 自定义 |
4. 实现模式
4.1 进入/退出动画
/* 向上滑动并淡入 */
@keyframes slideUpFadeIn {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
/* 使用 */
.element-enter {
animation: slideUpFadeIn var(--duration-normal) var(--ease-out) forwards;
}
4.2 弹簧物理
// 自然运动的弹簧预设
const springPresets = {
gentle: { stiffness: 120, damping: 14 },
wobbly: { stiffness: 180, damping: 12 },
stiff: { stiffness: 400, damping: 30 },
default: { stiffness: 300, damping: 20 }
};
4.3 加载状态
/* 脉冲动画 */
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* 旋转器 */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
animation: spin 1s linear infinite;
}
4.4 HUD效果
/* 发光脉冲 */
@keyframes glowPulse {
0%, 100% { box-shadow: 0 0 10px var(--color-primary-500); }
50% { box-shadow: 0 0 20px var(--color-primary-500), 0 0 30px var(--color-primary-500); }
}
.hud-glow {
animation: glowPulse 2s ease-in-out infinite;
}
4.5 交错动画
// 每个项目延迟50ms
const staggerDelay = (index: number) => index * 0.05
4.6 减少运动支持
/* 为减少运动偏好禁用动画 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
5. 实现工作流程(测试驱动开发)
步骤 1: 先编写失败测试
// tests/animations/modal.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import AnimatedModal from '~/components/AnimatedModal.vue'
describe('AnimatedModal', () => {
it('挂载时应用进入动画类', async () => {
const wrapper = mount(AnimatedModal, {
props: { isOpen: true }
})
expect(wrapper.classes()).toContain('modal-enter-active')
})
it('尊重减少运动偏好', async () => {
// 模拟 matchMedia
window.matchMedia = vi.fn().mockImplementation(query => ({
matches: query === '(prefers-reduced-motion: reduce)',
addEventListener: vi.fn(),
removeEventListener: vi.fn()
}))
const wrapper = mount(AnimatedModal, {
props: { isOpen: true }
})
expect(wrapper.classes()).toContain('reduced-motion')
})
it('在持续时间阈值内完成动画', async () => {
const wrapper = mount(AnimatedModal, {
props: { isOpen: true }
})
const style = getComputedStyle(wrapper.element)
const duration = parseFloat(style.animationDuration) * 1000
expect(duration).toBeLessThanOrEqual(300) // 模态框最多300ms
})
})
步骤 2: 实现最小可通过代码
<template>
<Transition name="modal">
<div
v-if="isOpen"
class="modal"
:class="{ 'reduced-motion': prefersReducedMotion }"
>
<slot />
</div>
</Transition>
</template>
<script setup lang="ts">
import { useReducedMotion } from '~/composables/useReducedMotion'
defineProps<{ isOpen: boolean }>()
const prefersReducedMotion = useReducedMotion()
</script>
步骤 3: 按照模式重构
- 将动画时序提取到设计令牌
- 仅添加GPU加速属性
- 确保在卸载时正确清理
步骤 4: 运行完整验证
# 运行动画测试
npm test -- --grep "animation"
# 检查布局颠簸
npm run lighthouse -- --only-categories=performance
# 验证减少运动支持
npm run test:a11y
6. 性能模式
模式 1: will-change 使用
/* 错误: 始终激活 will-change */
.animated-element {
will-change: transform, opacity;
}
/* 正确: 仅当动画时应用 */
.animated-element:hover,
.animated-element:focus,
.animated-element.is-animating {
will-change: transform, opacity;
}
/* 正确: 动画后移除 */
.animated-element {
transition: transform 0.3s ease;
}
.animated-element.animate-complete {
will-change: auto;
}
模式 2: Transform vs 布局属性
/* 错误: 触发布局重新计算 */
.sidebar-toggle {
width: 0;
transition: width 0.3s ease;
}
.sidebar-toggle.open {
width: 280px;
}
/* 正确: GPU加速的变换 */
.sidebar-toggle {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar-toggle.open {
transform: translateX(0);
}
模式 3: 硬件加速
/* 错误: 无GPU加速提示 */
.card {
transition: transform 0.3s;
}
/* 正确: 强制创建GPU层 */
.card {
transform: translateZ(0); /* 创建GPU层 */
backface-visibility: hidden;
transition: transform 0.3s;
}
/* 正确: 现代方法 */
.card {
contain: layout style paint;
transition: transform 0.3s;
}
模式 4: 减少运动处理
/* 错误: 忽略用户偏好 */
function animateElement(el: HTMLElement) {
el.animate([
{ transform: 'translateY(20px)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 }
], { duration: 300 })
}
/* 正确: 尊重偏好并有后备 */
function animateElement(el: HTMLElement) {
const prefersReduced = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches
if (prefersReduced) {
el.style.opacity = '1'
return
}
el.animate([
{ transform: 'translateY(20px)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 }
], { duration: 300 })
}
模式 5: 动画批处理
/* 错误: 多次回流 */
function animateItems(items: HTMLElement[]) {
items.forEach((item, i) => {
item.style.transform = `translateY(${i * 10}px)`
item.style.opacity = '0'
})
}
/* 正确: 批处理读取和写入 */
function animateItems(items: HTMLElement[]) {
// 读取阶段 - 批处理所有测量
const positions = items.map(item => item.getBoundingClientRect())
// 写入阶段 - 批处理所有突变
requestAnimationFrame(() => {
items.forEach((item, i) => {
item.style.transform = `translateY(${i * 10}px)`
item.style.opacity = '0'
})
})
}
/* 正确: 使用Web Animations API进行批处理 */
function animateItems(items: HTMLElement[]) {
const animations = items.map((item, i) =>
item.animate([
{ transform: 'translateY(0)', opacity: 0 },
{ transform: 'translateY(0)', opacity: 1 }
], {
duration: 300,
delay: i * 50,
fill: 'forwards'
})
)
return Promise.all(animations.map(a => a.finished))
}
7. 质量标准
性能要求
- 目标60fps(每帧16.67ms)
- 使用
transform和opacity进行动画 - 避免动画化
width、height、margin、padding - 谨慎使用
will-change
/* ✅ GPU加速属性 */
.animated {
transform: translateX(0);
opacity: 1;
transition: transform 0.3s, opacity 0.3s;
}
/* ❌ 导致布局颠簸 */
.animated-bad {
left: 0;
width: 100px;
transition: left 0.3s, width 0.3s;
}
8. 常见错误
/* ❌ 过度动画 */
* { transition: all 0.3s ease; }
/* ✅ 有目的的 */
.button { transition: background-color 0.2s ease, transform 0.1s ease; }
/* ❌ 太慢 */
.modal { animation: fadeIn 1s ease; }
/* ✅ 敏捷 */
.modal { animation: fadeIn 0.2s ease; }
/* ❌ 触发布局 */
.sidebar { transition: width 0.3s; }
/* ✅ 使用变换 */
.sidebar { transform: translateX(-100%); transition: transform 0.3s; }
13. 实现前检查清单
阶段 1: 编码前
- [ ] 动画目的明确定义
- [ ] 从令牌中选择目标持续时间和缓动
- [ ] 规划减少运动后备方案
- [ ] 为动画行为编写测试用例
- [ ] 建立性能预算(目标60fps)
阶段 2: 实现期间
- [ ] 仅使用 transform/opacity 进行动画
- [ ] will-change 有条件应用(非始终开启)
- [ ] 多元素动画批处理
- [ ] 添加硬件加速提示
- [ ] 实现减少运动媒体查询
阶段 3: 提交前
- [ ] 所有动画测试通过
- [ ] 性能验证为60fps(DevTools)
- [ ] 未检测到布局颠簸
- [ ] prefers-reduced-motion 测试
- [ ] 时序令牌使用一致
- [ ] 无诱发癫痫的闪烁(最大每秒3次)
14. 总结
您的目标是创建以下运动:
- 有目的的: 每个动画都有原因
- 性能优良: 所有设备上平滑60fps
- 无障碍: 尊重用户偏好
- 一致: 使用标准化令牌
运动应增强体验,而非分散注意力。好的动画感觉自然且几乎无形 - 用户完成任务时不会注意到运动,只会感到界面响应迅速且充满活力。
有目的地动画,卓越地执行,并始终尊重用户偏好。