纯CSS动画
简单模式,浏览器只靠 CSS 声明式规则就能渲染出动画效果。
基础过渡(transition)
transition 是最简单的 CSS 动画方式,用来在属性值变化时平滑过渡,能响应 CSS 属性的变化来源(伪类、class 切换、媒体查询、JS 动态修改等)。比如按钮悬停变色↓。
1
2
3
4
5
6
7
|
.button {
background-color: #4cafef;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #2196f3;
}
|
完整语法
1
|
transition: <property> <duration> <timing-function> <delay>;
|
- property:要过渡的属性,例如 opacity、background、transform
- duration:动画持续的时间,例如 0.3s、500ms
- timing-function:节奏曲线,例如linear、ease、cubic-bezier(x1, y1, x2, y2)
- delay:延迟开始的时间
因为它在动画里实在太常见了,transform 是专门用来做几何变换的属性,
提供:
- 平移 translateX(50px)、translateY(-20px)、translate3d(10px, 20px, 30px)
- 缩放 scale(1.5)、scaleX(2)、scaleY(0.5)、scale3d(1, 2, 1)
- 旋转 rotate(45deg)、rotateX(30deg)、rotateY(180deg)、rotate3d(1, 1, 0, 45deg)
- 倾斜 skewX(30deg)、skewY(15deg)
一个普通的旋转动画,可以用来“展开/收起”箭头之类的:
1
2
3
4
5
6
7
|
.icon {
transform: rotate(0deg);
transition: transform 0.4s ease-in-out;
}
.icon:hover {
transform: rotate(180deg);
}
|
使用关键帧(@keyframes)
@keyframes 就是一个“动画时间轴的定义”,它本身不会让元素动起来,必须有一个 引用它的机制(animation) 才能生效。
完整语法
animation 相比 transition,可以不依赖事件自动播放,有@keyframes带来的多个中间状态,控制也更多样。
1
2
3
|
selector {
animation: name duration timing-function delay iteration-count direction fill-mode play-state;
}
|
- name:动画名称,对应 @keyframes 的定义
- iteration-count:循环次数,例如 3, infinite
- direction:循环方向,normal,reverse,alternate,alternate-reverse
- fill-mode:动画执行前后,元素的样式:none,forwards,backwards,both
简单用例
一个滑动淡入淡出功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@keyframes move {
0% { transform: translateX(-200px); }
100% { transform: translateX(0); }
}
@keyframes fade {
0% { opacity: 0; }
100% { opacity: 1; }
}
.fade-in {
animation: fade 1s ease forwards;
}
// 多个动画叠加,飞入 + 淡入
.element {
animation: move 2s ease-out, fade 2s ease-out;
}
|
- transform:元素要变成 什么样子(例如:平移100px,缩放 1.2 倍)。
- @keyframes:定义 什么时候变成这样子(0% 到 100% 之间变化规则)。
- animation:让元素去执行 @keyframes 的时间轴。
1
2
3
4
5
6
7
8
9
|
@keyframes bounce {
0% { transform: translateY(0); }
50% { transform: translateY(-100px); }
100% { transform: translateY(0); }
}
.ball {
animation: bounce 1s ease infinite;
}
|
JS驱动的逐帧动画
这里能用的就不只是 CSS 属性了,适合复杂的交互场景。
requestAnimationFrame(callback):告诉浏览器 在下一次重绘之前 执行回调,并把当前的时间戳传进去。
- 逐帧调用:浏览器大约每秒 60 次刷新屏幕(60Hz 显示器),每帧调用一次。
- 与屏幕刷新同步:比 setTimeout更准确,不会掉帧或抖动。
- 节能:页面不可见(后台标签页)时会自动暂停,避免浪费性能。
简单用例
在一段时间内(intervalTime),让 container 从 startX 平滑滚动到 endX。
可以用来做轮播图,进度条,UI过渡动画之类的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
const startTime = Date.now()
const intervalTime = 500
const endTime = Date.now() + intervalTime
const tick = () => {
const nowTime = Date.now()
if(nowTime >= endTime) {
container.scrollTo(endX, 0)
return
}
const dx = ((nowTime - startTime) / intervalTime) * (endX - startX)
container.scrollTo(startX + dx, 0)
requestAnimationFrame(tick) // 每一帧结束时调用 tick 函数
}
tick()
|
进阶版:加了自定义的ease缓动函数 ,加了Promise可以在动画完成时触发后续逻辑,加了边界检测。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
const startTime = performance.now() // 比Date.now()精度高,更适合测量代码耗时、驱动动画
const startLocation = container.scrollLeft
const offset = endX - startX
new Promise(resolve =>
requestAnimationFrame(function step(currentTime) {
const timeElapsed = currentTime - startTime
const progress = Math.abs(options.duration ? Math.min(timeElapsed / options.duration, 1) : 1)
container.scrollLeft = Math.floor(startLocation + offset * ease(progress))
const clientWidth = container.clientWidth
const reachRight = container.scrollLeft + clientWidth >= container.scrollWidth
const reachLeft = container.scrollLeft <= 0 && offset < 0
if (progress === 1 || reachLeft || reachRight)
return resolve(offset) // 调用 resolve(offset) 后,Promise 状态变为 fulfilled
requestAnimationFrame(step) // 没结束就继续下一帧
}),
)
|
动画库
大多是基于 requestAnimationFrame 的封装,做风格统一调用方便的动画。
记录一下之前看过的 Motion 库。
抄文档和代码时发出疑问:这大部分看着都是在设置CSS动画,那它是JS动画库吗?是。并且提供了类似写 CSS 的 API。区别和优点可查阅:你还需要 Framer Motion 吗?
性能优化这一块,不是很懂,先用上以后慢慢研究吧…
一段简单用例
比如说,依然是一个滑动淡入淡出的装饰器,但是Motion实现版:
1
2
3
4
5
6
7
8
9
10
11
12
|
<motion.div
className={......}
initial={{
opacity: 0,
transform: 'translateX(20px)',
}}
animate={{ opacity: 1, transform: 'translateX(0px)' }}
exit={{ opacity: 0, transform: 'translateX(-20px)' }}
transition={{ duration: 0.3, ease: [0.42, 0.0, 0.58, 1.0] }}
>
{children}
</motion.div>
|
总的来说样式也是只用到了opacity和transform,看起来和CSS动画没什么差别。
但是考虑生命周期:initial → animate,animate → exit,就不一样了。
如果组件卸载是常有之事,比如做条件渲染,React 默认会立即卸载 <motion.div>,而Motion的<AnimatePresence>
会在条件变为 false 时先拦截这个卸载,先跑 animate → exit 动画,跑完再从 DOM 移除,这是CSS不好处理的~
配上 AnimatePresence 做条件渲染的版本:
1
2
3
4
5
6
7
8
9
10
11
|
<AnimatePresence mode="wait">
{open && (
<motion.div
key="panel"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3, ease: [0.42,0,0.58,1] }}
/>
)}
</AnimatePresence>
|
mode可选值:
- sync 进场动画和出场动画同时执行,新旧元素有重叠
- wait 等到旧元素完全消失,再执行进场动画
- popLayout 同时执行动画,但新元素直接顶替旧元素的位置
一段复杂用例
适合图标、Logo 动效、数据路径变形的动画,使用Motion自带的创建和组合运动值的钩子:
useMotionValue:一个可订阅的运动值,可以用useMotionValue手动创建。类似于 React state,但是值变化时,绑定到它的组件会直接更新样式属性(JS → DOM),不经过 React diff。
useTransform:用来把一个 MotionValue 的 输入范围 → 输出范围 做映射,返回值映射到这个输出范围里:useTransform(监控谁, 当监控的值在[这些范围]时, 返回[这些对应的值])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// 三个路径的连续变形
const path1 = "M10,10 H30 V30 H10 Z" // 正方形
const path2 = "M20,10 L30,20 L20,30 L10,20 Z" // 菱形
const path3 = "M20,10 A10,10 0 1,1 20,30 A10,10 0 1,1 20,10 Z" // 圆形
// 驱动动画的进度值,范围 0 → 1
const progress = useMotionValue(0)
useEffect(() => {
const control = animate(progress, 1, { duration: 0.3, ease: "linear" })
return () => controls.stop()
}, [])
// 会根据 `progress` 在 [0, 0.5, 1] 三个区间内插值
const path = useTransform(
progress,
[0, 0.5, 1], // 输入区间
[path1, path2, path3], // 输出路径
{
mixer: (a, b) => interpolate(a, b, { maxSegmentLength: 0.1 }) // 自定义插值器
}
)
return (
<motion.svg viewBox="0 0 40 40" width={120} height={120}>
<motion.path d={path} fill="currentColor" />
</motion.svg>
)
|
在 SVG 里,折线图、面积图、饼图、雷达图等等,本质上都是由 路径 (path) 描述出来的。
mixer 是一个函数,用来生成“两个值 a 和 b 之间的插值器”。interpolate 来自 Flubber 库,它能算出 a 和 b 之间的平滑形变。
用例中每当 progress 改变,Motion 自动更新 d,路径就会平滑地在形状之间过渡。
结果:
- progress=0:显示正方形
- progress=0.25:正在逐渐收缩成菱形
- progress=0.5:完全是菱形
- progress=0.75:菱形逐渐圆润化
- progress=1:完全是圆形