JS 原生高性能的动画实现:Web Animations API
我们常用的、给页面元素添加动画的方式之一是,使用 CSS Keyframes,通过百分比来控制属性值的变化,从而产生动画效果,但是这样有个缺点,就是 keyframes 是静态的,里边的属性值不能动态的设置,并且也没有办法暂停或改变动画的执行过程:
@keyframes FadeAndMove {
%0 {
opacity: 0;
transform: translateY(-20px);
}
100% {
opacity: 1;
transform: translateY(0px);
}
}
而如果用 JavaScript 操作 style 属性又会导致动画执行效率低下,浏览器需要频繁刷新图层,那么针对这种情况,浏览器规范中出现了 Web Animations API,它算是 CSS Keyframes 动画底层的渲染机制,所以执行效率与 CSS 动画一样。这个 API 已经存在很多年了,但是一直在优化改进中,它的定义动画的语法与 css keyframes 类似,并且调用方式和 jquery 也类似,上手用起来很容易。
示例
我们来看一个例子,我们定义一个标题和一个副标题:
- 标题文字动画为,渐隐渐现并向下移动的效果。
- 副标题动画为,扩展和渐隐渐现的效果,只有在鼠标点击的时候才会执行,并且只动画执行一次,动画的执行时间是标题动画的一半。
编写 HTML
我们先来看 HTML 结构,很简单,就是一个 class 为 container 的容器,里边放着 h1 表示的标题,class 为 title,h2 表示的副标题,class 为 subtitle。
<main>
<div class="container">
<h1 class="title">Welcome</h1>
<h2 class="subtitle">JavaScript Web Animations API</h2>
</div>
</main>
编写 CSS
CSS 里主要就是用 main 元素把整体布局进行居中,然后设置了一下文字居中对齐、文字颜色和大小:
.container {
text-align: center;
}
h1 {
font-size: 72px;
color: white;
}
h2 {
color: hsl(0deg, 0%, 80%);
font-size: 18px;
margin-top: 18px;
}
标题渐隐渐现和移动动画
我们先来看一下标题渐隐渐现并向下移动的动画,首先在 JS 里获取标题元素:
let title = document.querySelector(".title");
接着定义 keyframes,Web Animations API 中的 keyframes 是用包含一组对象的数组来表示的,每个对象是由要进行动画的 CSS 属性和值构成的,每个对象会平分执行时间,一般至少需要两个对象,一个表示开始,一个表示结束。我们这里定义了一个 fadeAndMove
keyframes,让透明度从 0 过渡到 1,位置从 -20px 过渡到 0px:
let fadeAndMove = [
{
opacity: 0,
transform: `translateY(-20px)`,
},
{
opacity: 1,
transform: `translateY(0px)`,
},
];
这里要注意的是,如果 CSS 属性有连字符,需要写成驼峰形式,例如 "background-color" -> "backgroundColor"。
有了 Keyframes,还需要定义动画执行时间,和执行函数,分别用 duration 和 easing 属性来表示,这里定义了 titleTiming
对象,动画执行时间为 2 秒,注意这里是使用毫秒数定义的,不像是 CSS 中用秒定义时间,然后 easing 动画执行函数设置为 "ease-in-out",如果不设置,默认就是 "linear",线性的:
let titleTiming = {
duration: 2000,
easing: "ease-in-out",
};
有了 Keyframes、执行时间和函数之后,就可以执行动画了。执行动画可以使用 DOM 元素的 animate() 方法,第 1 个参数接收 Keyframe 数组,第 2 个参数接收时间和执行函数对象,我们调用一下 title 的 animate() 方法,这样标题就有了动画效果:
title.animate(fadeAndMove, titleTiming);
副标题动画效果
再来看副标题的动画效果,它是一个扩展加渐隐渐现的效果,利用 letter-spacing 这个 CSS 属性来产生扩展效果,我们获取副标题 DOM 元素,并定义 Keyframes:
- letterspacing 从 -0.5em 这样一个比较窄的宽度,扩展到 "initial" 初始宽度。
- 透明度从 0 过渡到 1
let subTitle = document.querySelector(".subtitle");
let expand = [
{
letterSpacing: "-0.5em",
opacity: 0,
},
{
letterSpacing: "initial",
opacity: 1,
},
];
我们之前提到了,副标题的动画执行时间是标题动画的一半,我们可以直接定义成 1000,但这样如果修改了标题动画执行时间,还得修改副标题的执行时间,有没有办法直接获取标题的动画执行时间呢?答案是有的,在调用 animate() 方法后,它会返回一个对象,里边可以控制动画的执行,获取动画的参数和状态,那么这样我们可以获取 title 的动画执行对象 titleChange:
const titleChange = title.animate(fadeAndMove, titleTiming);
然后在副标题的执行时间中,通过 titleChange.effect.getComputedTiming().duration 来获取标题动画的执行时间,并在它的基础上除以 2,来获得副标题的动画执行时间:
let subTitleTiming = {
duration: titleChange.effect.getComputedTiming().duration / 2,
easing: "ease-in-out",
};
好,现在副标题的动画 Keyframes 和时间、函数都有了,不过它要求是在鼠标点击的时候才能播放动画,那么,我们这里可以先调用 animate() 方法,然后在返回的动画对象中,调用 pause() 方法来暂停动画:
const subTitleChange = subTitle.animate(expand, subTitleTiming);
subTitleChange.pause();
接着给 document 添加事件,监听鼠标点击,在事件触发时判断,如果动画的状态不是已完成,那么就播放动画。判断动画的状态,可以利用动画对象中的 playState 属性,他有:
- idle 空闲
- running 正在执行
- paused 暂停
- finished 已完成
这四种状态,当动画执行完毕后就会变成 finished 状态,这里我们就判断,如果动画已经执行过一次了,就不做任何操作,如果是其它状态,就调用 play() 方法来执行动画:
document.addEventListener("click", () => {
if (subTitleChange.playState !== "finished") {
subTitleChange.play();
}
});
现在,就可以看到之前要求的动画效果了。
兼容性
Web Animations API 的兼容性可以参考 caniuse 网站的表格:
总结
好了,这节课我们介绍了 Web Animations API 的特点、用法,并且用了一个示例来说明如何定义 keyframes,时间和动画执行函数,以及使用 animate() 方法来执行动画。另外还介绍了如如何获取其它动画的一些属性和参数、暂停和播放动画,以及获取动画的状态。你学会了吗?如果有帮助请三连,想学更多有用的前端开发知识,请关注峰华前端工程师,感谢观看!
一系列的课程让你成为高级前端工程师。课程覆盖工作中所有常用的知识点和背后的使用逻辑,示例全部都为工作项目简化而来,学完即可直接上手开发!
即使你已经是高级前端工程师,在课程里也可能会发现新的知识点和技巧,让你的工作更加轻松!
《React 完全指南》课程,包含 React、React Router 和 Redux 详细介绍,所有示例改编自真实工作代码。点击查看详情。
《Vue 3.x 全家桶完全指南与实战》课程,包括 Vue 3.x、TypeScript、Vue Router 4.x、Vuex 4.x 所有初级到高级的语法特性详解,让你完全胜任 Vue 前端开发的工作。点击查看详情。
《React即时通信UI实战》课程,利用 Storybook、Styled-components、React-Spring 打造属于自己的组件库。
《JavaScript 基础语法详解》本人所著图书,包含 JavaScript 全面的语法知识和新特性, 可在京东、当当、淘宝等各大电商购买