本文也发布在知乎
zhuanlan.zhihu.com/p/373244605
开发移动端项目时,特别是活动页面,免不了会涉及动画。而且好的动画会给产品加分。本文总结了 web 端常见动画实现的方式,希望读者在阅读完本文后,掌握动画开发技巧,更加自信地解决交互设计师提出的需求。
Toolbox
实现 web 端动画有多种方式,常见的有 GIF/APNG,CSS3,JavaScript,lottie,SVG,canvas 等。具体在业务中使用何种方式,需要综合考虑实现成本与运行效率。需要对动画进行较多控制的,使用 JavaScript 或 lottie,其他使用 CSS3 与 GIF/APNG。
在上述的动画方式中,GIF/APNG 无疑最省力。相较于 GIF,APNG 更有优势(参考
这篇文章
)。简单的场景下,使用 APNG 与 setTimeout 也可以实现流程的控制。 不过于在实际使用的时候,注意以下问题:
APNG 预加载
通常 APNG 图片尺寸较大,最好提前加载。
APNG 不重复播放
已缓存的 APNG 图片再次显示时,动画不播放。你可以在链接中通过 query 参数来重新加载图片。
Codepen 示例
大家应该已经在项目中广泛应用 CSS3 动画,transition/animation 是动画利器。常见的过渡效果通过 CSS 都可以实现。如果没有思路的话,不妨去
Animate.css
或者
Animista.net
看看,也许答案就在上面。
animation-fill-mode
该属性设置在动画结束后,保持终止状态还是恢复初始状态。经常使用
animation-fill-mode: forwards;
来保持终止状态。
animation-function 中的 steps 函数
一般情况下,动画的过渡是连续的。通过
steps
函数可以让动画「断断续续」(每次切换一帧),实现帧动画的效果。借助该特性,实现一个简单的倒计时。
Codepen 示例
好用的 clip-path
clip-path
属性用于控制元素的显示区域。虽然使用
overflow: hidden;
也可以实现部分效果,但是代码量会增多,且对于多边形的裁剪就无能为力。
Codepen 示例
尽可能多地使用
transform
,有需要时使用
will-change
属性。如果同时要对大量的 DOM 元素做动效,或许你应该尝试使用 Canvas 而非 CSS。
Vue 的封装
Vue (特指 2.x) 对于动画进行了封装,提供 transition 与 transition-group 组件,过渡/动画用一套 API。Vue 中的过渡两个特性值得关注:
设置 mode 可以同时对离开与出现的元素添加过渡效果。
Codepen 示例
transition-group
当你要给多个元素做动画时,可以使用 transition-group组件。移动端场景中常见的跑马灯使用该组件实现。
Codepen 示例
Lottie、SVG
对于复杂的动画,推荐使用
Lottie
,并且动画制作软件 AE 支持导出 Lottie 文件。Lottie 使用方式极其简单,导入 JSON 文件完成动画的创建。
import lottie from 'lottie-web';
import animData from './animData.json';
const anim = lottie.loadAnimation(animData);
使用 lottie-api ,你甚至可以编辑原有的动画。 Codepen 示例
Lottie 相当于使用 JS 来播放动画,你可以对动画的播放速度,次数,帧数,顺序进行精准的控制。事件或者方法参考 官方文档 ,这里不再赘述。
如果 Lottie JSON 文件引入图片资源,要去调整图片的路径,避免出现 404 问题。当然更好的方法是使用自动化工具比如 lottie-loader 来处理 JSON 文件,调整图片路径。在使用了 lottie-loader 的前提下,你可以直接把 JSON 文件当做组件来用。
import MyAnimate from './data.json'
export default {
components: { MyAnimate }
Lottie JSON 字段含义
遗憾的是, Lottie 官方文档并没有介绍 JSON 中各字段的含义,只能在代码仓库中找到一些 JSON schema 描述文件 。下面对部分字段做简要描述,当你有定制化需求时,或许需要:
"fr": 20,
"ip": 0,
"op": 40,
"w": 700,
"h": 500,
"assets": [{
"w": 120,
"h": 120,
"u": "images/",
"p": "img_0.png"
由 fr
, ip
与 op
可知动画播放一个周期需要 2s。通过调用 setSeed(2)
可以将播放时间降为 1s;通过 play(frame)
或者 goToAndPlay(frame)
类似的方法设置动画从某一帧开始播放。
你可以编写一个简单的 lottie-loader 来处理 assets 中的图片路径
动画实践指南
借助上文中提到了多种工具,处理简单的动画不在话下。对于稍微复杂的动画,要用 JS 去编写动画逻辑。以下面的动画效果为例,来讲解实现思路。
效果图尺寸较大,可以点击 这里 预览效果。
通过分析发现,主要的逻辑集中在红包上。红包有停止,暂停两种状态;按照某个频率从顶部还会落下新的红包;按照某个频率移除可视区范围外的红包;移动的过程中旋转红包。 编写代码时,为了提高灵活性,最常用的策略是数据抽象与过程抽象。先进行数据抽象,把红包雨整个区域用数据来描述。
class Stage {
children: Packet[]
duration: number
destroyed: boolean
class Packet {
x: number
y: number
degree: number
rotation: 'clockwise' | 'anticlockwise'
status: 'idle' | 'moving' | 'removed'
接下来,进行过程抽象,添加方法,让数据动起来。由于存在多种频率不同的动画,把一种动画想象成一个 task,用 async 函数描述过程。
class Stage {
init() { }
async add() {
while (true) {
if (this.destroyed) return
await wait(200)
async animateAndClear() { }
class Packet {
stop() { }
通过前面两步,完成了对整个动效的抽象。此时,动画已经是「运动」的了。接下来都是 View 层的工作。借助我们已经掌握的技能,View 层不难实现。 在项目开发过程中,使用的框架是 Vue。如果你想用 Canvas 去实现,数据层可以复用,只需要针对 View 层做适配工作即可。
状态的复用与组件抽象
React 与 Vue3 都提供了对 Hook 的支持,数据或者处理数据的逻辑(Hook)是可以复用的。在基础组件库日益丰富的当下,View 层的工作以组合基础组件为主,相当多的业务逻辑是处理数据。回到刚才的例子,我们将先考虑状态的设计,再剥离状态,类似于对 Lottie/Hook 的简单模仿。毕竟调整数据的配置比编写业务代码成本低得多。
Hook 与动画
Vue 的封装/mode小节中的示例如果用 Hook( ReactSpring ) 来实现的话,核心代码如下:
import { useTransition, animated } from "react-spring";
export default function App() {
// ...
const transitions = useTransition(show, null, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
return <div>...</div>
完整的代码
从上面的例子可以看出,我们只需要设置好初始与终止状态,然后再与 View 层做绑定,整个动画就完成了,便捷程度堪比 CSS3。在先前 Vue 的实现中,依赖 transition 组件的功能,而 ReactSpring 则是整个过渡的状态提供给你,你决定如何去渲染。
如果后续需要对该场景进行封装,那么大概会这样拆分:
过渡状态抽象成 Hook,对外导出
基于 Hook 实现一个组件,默认导出该组件
用户如果对组件不满意,是完全可以基于 Hook 实现新的组件,代码量并不大。
随着 Hook 概念的推广,我们也要逐渐学习掌握它。开发项目过程中,有意识地去思考如何对数据进行抽象,如何更好的管理数据。
本文未涉及的内容
本文主要讨论的是 web 端简单的动画实现方式。对于更为复杂的场景,如 HTML 5 游戏,为了性能与开发效率的考量,建议使用 pixi.js 或者 phaser 之类的游戏框架。
丰富你的工具箱,掌握更多工具
对需求进行拆分,对数据抽象/过程进行抽象,提高可维护性
学习 Hook,思考状态逻辑如何复用
ziyoung
web 前端
粉丝