引言
动画在前端开发中扮演着重要的角色。它不仅可以提升用户体验,还可以使界面更加生动和有趣。在这篇文章中,我们将深入探讨前端动画的各种实现方式,包括 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" });
代码示例:Web Animations API 动画
关于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>