引言
动画在前端开发中扮演着重要的角色。它不仅可以提升用户体验,还可以使界面更加生动和有趣。在这篇文章中,我们将深入探讨前端动画的各种实现方式,包括 CSS 动画、JavaScript 动画、SVG 动画等。我们还将讨论一些触发动画的方式和动画在用户体验中的最佳实践。
前端动画分类
CSS 动画
- CSS Transition
CSS 过渡,属于补间动画,即设置关键帧的初始状态,然后在另一个关键帧改变这个状态,比如大小、颜色、透明度等,浏览器将自动根据二者之间帧的值创建的动画。 - CSS Animation
CSS 动画,可以理解是CSS Transition
的加强版,它既可以实现 补间动画 的动画效果,也可以使其以 逐帧动画 的方式进行绘制。
- CSS Transition
SVG 动画
- SVG 动画用于矢量图形,提供了高质量的动画效果,常用于图标和图形动画。可以使用 SMIL 在SVG中定义动画。同样的也可以使用css或者js来控制svg动画。
Canvas 动画
- 通过结合使用
requestAnimationFrame
、路径和变换等技术对画布的元素进行擦除和重新绘制,可以实现复杂的动画效果。另外Canvas还可以用于绘制复杂的背景或静态内容,从而减少每帧的绘制工作量。 - 可以参考我的一篇关于canvas制作动画的文章:用Canvas绘制一个高可配置的圆形进度条
- 通过结合使用
JS 动画
setTimeout / setInterval / requestAnimationFrame
setTimeout
和setInterval
这两个 API 设定的时间会因为浏览器当前工作负载而有所偏差,而且无法与浏览器的绘制帧保持同步。所以才有了 与浏览器的绘制帧同步 的原生 APIrequestAnimationFrame
,以取代setTimeout
和setInterval
实现动画。Web Animations API
浏览器动画 API,通过 JavaScript 操作。这些 API 被设计成CSS Transition
和CSS Animation
的接口,很容易通过 JS 的方式实现 CSS 动画,它是对动画化的支持最有效的方式之一。
css 动画
css过渡动画 transition
注意
由于浏览器是根据样式差异化的两帧自动计算并过渡,所以 transition
只支持可识别中间值的属性 (如大小、颜色、位置、透明度等),而如 display 属性则不支持。
语法定义
transition-property
: 指定哪个或哪些 CSS 属性用于过渡。只有指定的属性才会在过渡中发生动画,其他属性仍如通常那样瞬间变化。transition-duration
: 指定过渡的时长。你可以为所有属性指定一个值,或者指定多个值,或者为每个属性指定不同的时长。transition-timing-function
: 指定一个缓动函数,定义属性值怎么变化。常见的缓动函数是一个三次贝塞尔曲线 (cubic-bezier(<x1>, <y1>, <x2>, <y2>)
)。当然也可以选择关键字- linear:
cubic-bezier(0.0, 0.0, 1.0, 1.0)
- ease:
cubic-bezier(0.25, 0.1, 0.25, 1.0)
- ease-in:
cubic-bezier(0.42, 0.0, 1.0, 1.0)
- ease-out:
cubic-bezier(0.0, 0.0, 0.58, 1.0)
- ease-in-out:
cubic-bezier(0.42, 0.0, 0.58, 1.0)
- linear:
transition-delay
: 指定延迟,即属性开始变化时与过渡开始发生时之间的时长。
代码示例
/* 单条 简写形式 */
transition:
<property> <duration> <timing-function> <delay>;
/* 多条 简写形式 */
transition:
<property> <duration> <timing-function> <delay>,
<property> <duration> <timing-function> <delay>,
...;
/* 单条 子属性形式 */
transition-property: <property-name>;
transition-duration: <duration-time>;
transition-timing-function: <timing-function>;
transition-delay: <duration-time>;
/* 多条 子属性形式 */
transition-property: <property-name> [, <property-name>, ...];
transition-duration: <duration-time> [, <duration-time>, ...];
transition-timing-function: [, <cubic-bezier>, ...];
transition-delay: [, <duration-time>, ...];
// 如果任意属性值列表的长度比其他属性值列表要短,则其中的值会重复使用以便匹配
// 如果某个属性的值列表长于 `transition-property` 的属性,则将被截短
css过渡动画 触发方式
1. 伪类触发(:hover、:focus、:active等)
.button {
background-color: blue;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: red;
}
2. 类名切换(通过JS动态切换类名来触发过渡效果)
<button id="toggleButton">Toggle</button>
<div id="box" class="box"></div>
<style>
.box {
width: 100px;
height: 100px;
background-color: blue;
transition: background-color 0.3s ease;
}
.box.active {
background-color: red;
}
</style>
<script>
document.getElementById('toggleButton').addEventListener('click', function() {
document.getElementById('box').classList.toggle('active');
});
</script>
3. 属性变化
<button id="toggleButton">Toggle</button>
<div id="box" class="box"></div>
<style>
.box {
width: 100px;
height: 100px;
background-color: blue;
transition: background-color 0.3s ease;
}
</style>
<script>
document.getElementById('toggleButton').addEventListener('click', function() {
const box = document.getElementById('box');
box.style.backgroundColor = box.style.backgroundColor === 'red' ? 'blue' : 'red';
});
</script>
4. 伪元素触发(通过伪元素如::before
、::after
的状态变化来触发过渡效果。)
<div class="box"></div>
<style>
.box {
width: 100px;
height: 100px;
position: relative;
}
.box::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: blue;
transition: background-color 0.3s ease;
}
.box:hover::before {
background-color: red;
}
</style>
css动画 animation
注意
CSS Animation 具备了对 关键帧和循环次数 的自定义能力。CSS Animation 在实现像 CSS Transition 补间动画 效果时,还可以在起始帧和结束帧之间自定义中间帧,使得动画更加平滑过渡的同时,对动画有了更好的控制和自定义能力。
语法定义
先创建一个带名称的 @keyframes
规则,以便后续使用 animation-name
属性将动画同其关键帧声明进行匹配。每个规则包含多个关键帧,也就是一段样式块语句,每个关键帧有一个百分比值作为名称,代表在动画进行中,在哪个阶段触发这个帧所包含的样式。
animation-name
:指定一个或多个 @keyframes 的名称,描述了要应用于元素的动画。多个 @keyframes 以逗号分隔。animation-duration
:设置动画完成一个动画周期所需的时间,需要指定单位,如1s
、500ms
。animation-delay
:指定执行动画之前的等待时间。动画可以稍后开始、立即从开头开始、立即在动画中途播放 (如-1s
) 。其中-1s
意思是动画立即从 1s 处开始。animation-iteration-count
:设置动画序列在停止前应播放的次数,有效值0
、正整数、正小数、无限循环infinite
。animation-direction
:设置动画是正向播放normal
、反向播放reverse
、正向交替播放alternate
、反向交替播放alternate-reverse
。animation-play-state
:设置动画是运行还是暂停,有效值running
、paused
。animation-fill-mode
:设置 CSS 动画在执行之前和之后如何将样式应用于其目标,有效值如下:none
:当动画未执行时,动画将不会将任何样式应用于目标,而是已经赋予给该元素的 CSS 规则来显示该元素。这是默认值forwards
:目标将保留由执行期间遇到的最后一个关键帧计算值。backwards
:动画将在应用于目标时立即应用第一个关键帧中定义的值。
animation-timing-function
:设置动画在每个周期的持续时间内如何进行,主要是如下两种函数:
cubic-bezier
三次贝塞尔曲线 (cubic-bezier(<x1>, <y1>, <x2>, <y2>)
),以实现 补间动画 效果。steps
是一个分段的阶跃函数,,以实现 逐帧动画。n 相当于单次动画的帧数,每帧动画的时间是均等的 (steps(n, <jumpterm>)
),其中jumpterm (默认值 end)
含义如下:- jump-start:在起始位置阶跃,
n=2 ⇒ 50% 100%; (100 / 2)
- jump-end:在结束位置阶跃,
n=4 ⇒ 0% 25% 50% 75%; (100 / 4)
- jump-none:起止位置均无跳跃,
n=5 ⇒ 0% 25% 50% 75% 100%; (100 / 4)
- jump-both:起止位置均有跳跃
n=3 ⇒ 25% 50% 75%; (100 / 4)
- start:等同 jump-start
- end:等同 jump-end
- step-start:等同 steps(1, jump-start)
- step-end:等同 steps(1, jump-end)
- jump-start:在起始位置阶跃,
/* animation 声明样式顺序 */
/* animation-duration */
/* animation-easing-function */
/* animation-delay */
/* animation-iteration-count */
/* animation-direction */
/* animation-fill-mode */
/* animation-play-state */
/* animation-name */
animation: 3s ease-in 1s 2 reverse both paused slidein;
/* animation - duration | easing-function | delay | name */
animation: 3s linear 1s slidein;
/* more animations - duration | easing-function | delay | name */
animation: 3s linear slidein, 3s ease-out 5s slideout;
/* animation-name */
animation-name: none;
animation-name: animate1;
animation-name: animate1, animate2;
/* animation-timing-function */
animation-timing-function: ease;
animation-timing-function: step-start;
animation-timing-function: cubic-bezier(0.1, 0.7, 1, 0.1);
animation-timing-function: ease, step-start, cubic-bezier(0.1, 0.7, 1, 0.1);
css animation 动画触发方式
和css transition
触发动画方式相似
此外还可以增加一个图层,专门用于制作动画效果。
例如:鼠标在点击按钮时,会有涟漪动画。
// 涟漪动画定义
@keyframes ripple {
0% {
transform: scale(0);
opacity: 1;
}
to {
transform: scale(4);
opacity: 0;
}
}
// 图层动画 css
.ripple {
position: absolute;
border-radius: 50%;
background: rgba(8, 7, 7, 0.2);
pointer-events: none;
animation: ripple 0.6s linear;
}
// 制作动画 这样每次点击按钮 就会生成动画,动画结束便销毁动画元素
const makeAnimate = (e: React.MouseEvent) => {
const dom = e.currentTarget;
const rect = dom.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const size = 100;
const ripple = document.createElement('span');
ripple.classList.add('ripple');
ripple.style.width = `${size}px`;
ripple.style.height = `${size}px`;
ripple.style.left = `${x - size / 2}px`;
ripple.style.top = `${y - size / 2}px`;
dom.appendChild(ripple);
ripple.addEventListener('animationend', () => {
ripple.remove();
});
};
svg 动画
常用的 SMIL 动画元素
<animate>
:用于动画化单个属性。<animateTransform>
:用于动画化变换属性,如旋转、缩放、平移等。<animateMotion>
:用于沿着路径动画化元素。(路径动画)<set>
:用于在指定时间点设置属性值。
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" fill="red">
<animate attributeName="cx" from="50" to="150" dur="2s" repeatCount="indefinite" />
</circle>
</svg>
svg 描边动画
SVG动画的路径实现主要依赖属性:stroke
(描边)和 fill
(填充)。
- stroke:定义svg的轮廓线。常用css属性有:
stroke-dasharray
(描边的样式),stroke-dashoffset
(起始位置),stroke-color
(描边的颜色),stroke-opacity
(描边的透明度),stroke-linecap
(描边端点形状)等。 - fill:定义svg内部颜色或图案 ,常用css属性有
fill-opacity
(定义填充的透明度),fill-rule
(定义填充规则)等。
stroke-dasharray (定义虚线的长度和间隔)
提供一个奇数或偶数数列,其中数与数之间用逗号或空格隔开,用来指定短划线和缺口的长度,并重复。 如果是偶数数列
,则一个表示短线长度,一个表示缺口长度。 如果是奇数数列
,将奇数数列复制一个变成偶数数列,然后按照短线,缺口的顺序绘制。
(偶数数列) stroke-dasharray="5, 5" x1="10" y1="10" x2="190" y2="10"
表示从坐标(10,10)到(200,10)这条水平线上,短划线和缺口都为5个px
(奇数数列) stroke-dasharray="20 10 5" x1="10" y1="10" x2="190" y2="10"
表示从坐标(10,10)到(200,10)这条水平线上,短划线和缺口按照20 10 5 20 10 5的顺序排列。
stroke-dashoffset (定义虚线的起始位置)
stroke-dashoffset 属性用于指定路径开始的距离(正值向左偏移,负值向右偏移)
描边动画示例:svg描边动画
js 动画
setTimeout / setInterval API
设定定时器,通过周期性的触发重复执行绘制动画的函数,来实现 “逐帧动画” 的效果。
优势
- 具有很好的浏览器兼容性
劣势
- 只能接近设备屏幕刷新率,无法做到和浏览器同步,所以可能会存在卡顿、丢帧、抖动的现象
- 由于浏览器单线程机制,存在队列中回调函数被阻塞的可能,所以无法保证每一次调用的时间间隔都相同,某次回调可能会被跳过,导致跳帧。
requestAnimationFrame API
为了弥补 setTimeout / setInterval
在动画方面的不足,浏览器提供了为动画而生的 API,它可以让 DOM 动画、Canvas 动画、 SVG 动画等有一个统一的刷新机制,随着浏览器的屏幕刷新,统一绘制动画帧。
let id = null
// 动画函数
const draw = () => {
/* 动画绘制... */
}
const start = () => {
draw()
cancelAnimationFrame(id)
id = requestAnimationFrame(start)
}
const stop = () => { cancelAnimationFrame(id) }
优势
- 由系统来决定回调函数的执行时机, 它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次, 这样就不会引起丢帧现象, 也不会导致动画出现卡顿的问题。
- 在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU的开销。
不足
- 同 setTimeout/setInterval 一样,它是以逐帧动画的方式进行绘制,无法做到像 CSS 动画,让游览器自动根据两帧之间的差异创建插值,以实现补间动画的过渡效果。
Web Animations API
requestAnimationFrame
、setTimeout/setInterval
都是以逐帧绘制的方式实现动画, 而 Animations API 不仅可以 “逐帧动画”,还可以实现 “补间动画” 的效果。- CSS 动画有一定的局限性,需要事先预设动画样式,而且无法与 JS 进行交互。相比之下,Animations API 可以随时定义并使用动画,自然是更加灵活方便。
参考文档: https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Animations_API
语法示例:
const element = document.getElementById("container");
const animation = element.animate(
[
{ transform: "translateY(0%)" },
{ transform: "translateY(100%)" },
],
{ duration: 3000, fill: "forwards" }
);
关于Filp动画
浏览器计算位置很快,绘制可能很慢。利用浏览器强大的计算能力,获取动画的起止状态,接着单独开启一个线程做动画。这样触发布局更新的操作,只会发生在一帧时间内,剩下的动画跑在单独的线程上,会更流畅。
介绍下FLIP 。
- F 代表 First,也就是动画的开始状态。
- L 代表 Last,代表动画结束状态。
- I 代表 Invert,也就是状态反转,使用 transform 等属性,创建单独的图层,并将元素状态反转回去。
- P 代表 Play,播放动画。
示例代码:
其中,在初始帧中,应用逆变换(translate
和 scale
),将元素从其最终状态逆变换到初始状态。
最后一帧 transform: "none"
的作用是将元素的变换属性重置为其最终状态。具体来说,transform: "none"
表示不应用任何变换,这意味着元素将恢复到由 CSS 设置的最终位置和大小。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FLIP Animation Example</title>
<style>
#box {
width: 100px;
height: 100px;
background-color: #4caf50;
position: absolute;
}
</style>
</head>
<body>
<div id="box"></div>
<button id="animateButton" style="margin-top: 300px;">Animate</button>
<script>
const box = document.getElementById('box');
const button = document.getElementById('animateButton');
button.addEventListener('click', () => {
// First: 记录初始状态
const first = box.getBoundingClientRect();
// 修改元素的位置
box.style.top = `${300}px`;
box.style.left = `${300}px`;
// Last: 记录最终状态
const last = box.getBoundingClientRect();
// Invert: 计算初始状态和最终状态之间的变换
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaW = first.width / last.width;
const deltaH = first.height / last.height;
// 应用 FLIP 动画
box.animate(
[
{
transformOrigin: "top left",
transform: `
translate(${deltaX}px, ${deltaY}px)
scale(${deltaW}, ${deltaH})`,
},
{
transformOrigin: "top left",
transform: "none",
},
],
{
duration: 300,
easing: "ease-in-out",
fill: "both",
}
);
});
</script>
</body>
</html>