前言
好久没更新博客了,这段时间算是给自己放了一个小长假吧,还记得上期在中国象棋中使用到的bigText的动画特效吗? 没错,这期我详细讲解一个比那个动画特效更加复杂的一个bigText的动画特效,并且从多种方式层层递进式的进行实现讲解。最终效果图如下:(后续补上,大家可以先直接运行最终代码看目标效果)(js动画特效转gif文件好难顶啊,有没有大佬有其它更好的方式)
实现思路
仔细观察动画,不难得到以下三个重要细节:
- 整个过程可以粗略分成三步:文字进入,短暂停歇,文字消失。
- 在文字进入阶段,文字列表的内容是陆续呈现;在文字消失阶段,字列表的内容是同步消失。
- 文字列表单体之间的动画效果(文字进入和文字消失)是一样的。
得到了上述的消息后,我们不妨先从单个文字的效果进行实现,然后再进行扩展。
keyframes
keyframes核心
实现动画效果,当然是实现方式越简单越好,首当其冲的是只使用css进行那必然是最佳。 显然,这个动画效果只使用css transition过渡动画是远不够的,所以我们进一步,尝试使用css keyframes进行实现。 在开始讲述之前,不熟悉keyframes使用方式的朋友可以先查看官方文档:keyframes官方文档 在这里我进行一些简要的使用描述:keyframes通过定义每一种关键帧的css样式进而实现这一段时间的动画效果。 所以使用keyframes的重点也是难点在于需要捕获目标动画的所有不同状态的样式,并将其分别一一映射到keyframes的关键帧中。
实现思路
细节观察动画细节,不难捕获到的两组重要状态。(重点关注哪些特征发生变化) 文字进入阶段:
- 文字的透明度为0,并且其位置是在正常位置的下方。
- 文字完全显示,变大了1.25倍,并且回到的默认位置。
- 文字的透明度又变小了,并且进入停歇阶段。
文字消失阶段:
- 文字的透明度变为0,并且其位置是在正常位置的上方。
知道了所有的变化状态,那么接下来只需要将变化状态与关键帧进行配对即可。 Talk is cheap.show you my code:
@keyframes fade {
0% {
opacity: 0;
line-height: 45px;
transform: scale(1);
}
30% {
opacity: 1;
line-height: 16px;
transform: scale(1.25);
}
60%,
80% {
opacity: 0.6;
transform: scale(1);
line-height: 16px;
}
100% {
opacity: 0;
}
}
小细节
最后文字消失时候的位置变化是无法通过line-height来达到效果,这里我是使用改变父元素的的margin-top来达到上升的效果,但是因此这就意味着我们还需要给父元素再编写一个同步的文字消失动画:
@keyframes move{
80% {
margin-top: 100px;
}
100% {
margin-top: 70px;
}
}
扩展
到此,已经使用css keyframes完成了单个元素完整的动画流程,接下来尝试进一步将其扩展到完整的列表元素。 完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box2 {
width: 100%;
display: flex;
justify-content: center;
flex-wrap: wrap;
overflow: hidden;
margin-top: 100px;
animation: move 1.5s ease-in 1;
}
.item {
width: 100%;
height: 16px;
font-family: PingFangSC-Regular;
text-align: center;
line-height: 16px;
color: #383838;
opacity: 0;
letter-spacing: 0;
margin-bottom: 15px;
animation: fade 1.5s ease-in 1;
}
@keyframes fade {
0% {
opacity: 0;
line-height: 45px;
transform: scale(1);
}
30% {
opacity: 1;
line-height: 16px;
transform: scale(1.25);
}
60%,
80% {
opacity: 0.6;
transform: scale(1);
line-height: 16px;
}
100% {
opacity: 0;
}
}
@keyframes move{
80% {
margin-top: 100px;
}
100% {
margin-top: 70px;
}
}
</style>
</head>
<body>
<div class="box2">
<div class="item">
<span>前端开发</span>
</div>
<div class="item">
<span>后端开发</span>
</div>
<div class="item">
<span>全栈开发</span>
</div>
</div>
</body>
</html>
运行此代码,所呈现出的动画效果已经和目标效果已经非常相似了,唯独缺少目标效果中陆续出现的动画效果,那么最后这个效果又应该如何解决呢?
解决思路
只使用keyframes也是能够解决的,一个比较直接的解决方案是根据陆续呈现的时间差,为每一个列表元素写一个自己特定的动画(这种方案的具体实现大家可以自行尝试)。但是明明是一样的动画效果,只是触发时机的差异,难道就无法复用相同的动画函数吗?答案肯定是可以的,那么怎么复用,复用的思路来源于哪里,接下来,我们不妨先看看vue transition组件里的奥秘。
Vue transition
简介
使用Vue写过动画的朋友应该对transition组件并不陌生,它是Vue自带的专门为绘制动画而出现的组件。它的功能其实可以近似类比于css的transition即过渡,只不过Vue的transition组件里面定义了更多的过渡状态,并且每种过渡状态下都可以自定义其样式效果。 总共有六种过渡状态:v-enter、v-enter-active、v-enter-to、v-leave、v-leave-active以及v-leave-to。 有关Vue transition的详细使用教程可以参看Vue的官方文档:Vue transition文档
实现思路
接来下,我们尝试使用Vue transition来进行实现。 细心的大家应该不难发现,在keyframes中的变化状态其实就是对应着Vue transition里面的过渡状态,只不过这里的变化状态比transition里面的过渡状态的情形更多,所以也并不能完全拆解。 css代码如下:
.fade-enter-to {
animation: enter-in 1s ease-in 1;
}
@keyframes enter-in {
0% {
line-height: 45px;
transform: scale(1);
}
40% {
opacity: 1;
line-height: 16px;
transform: scale(1.25);
}
60%,
80% {
opacity: 0.6;
transform: scale(1);
line-height: 16px;
}
100% {
opacity: 0.6;
}
}
.fade-leave-to {
opacity: 0;
margin-top: 70px;
transition: all ease-in 0.4s;
}
代码重构
使用Vue重构上述 css @keyframes的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="./vue-2.5.22.min.js"></script>
<title>Document</title>
<style>
#app {
overflow: hidden;
}
.box2 {
width: 100%;
display: flex;
justify-content: center;
flex-wrap: wrap;
overflow: hidden;
margin-top: 100px;
}
.item {
width: 100%;
height: 16px;
font-family: PingFangSC-Regular;
text-align: center;
line-height: 16px;
color: #383838;
opacity: 1;
letter-spacing: 0;
margin-bottom: 15px;
}
.fade-enter-to {
animation: enter-in 1s ease-in 1;
}
@keyframes enter-in {
0% {
line-height: 45px;
transform: scale(1);
}
40% {
opacity: 1;
line-height: 16px;
transform: scale(1.25);
}
60%,
80% {
opacity: 0.6;
transform: scale(1);
line-height: 16px;
}
100% {
opacity: 0.6;
}
}
.fade-leave-to {
opacity: 0;
margin-top: 70px;
transition: all ease-in 0.4s;
}
</style>
</head>
<body>
<div id="app">
<template>
<transition name='fade' :duration="{ enter: 1000, leave: 400 }" v-on:before-enter="beforeEnter"
v-on:after-enter="afterEnter">
<div class="box2" v-if="show">
<div class="item" v-for="item in list">
<span>{{item}}</span>
</div>
</div>
</transition>
</template>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
show: false,
list: ['前端开发', '后端开发', '全栈开发']
},
mounted: function () {
this.show = !this.show;
},
methods: {
beforeEnter: function (el) {
el.style.opacity = 0;
},
afterEnter(el) {
el.style.opacity = 0.6;
this.show = !this.show;
}
}
})
</script>
</body>
</html>
结果
运行上述代码,我们会发现效果和目标效果不一致,原因很简单,line-height对于外层div并不会生效,所以上述transition里的内容需要进行拆分,外层div使用单独的transition,内层的list使用transition-group。 这种方案的具体实现大家也可以自行尝试,由于最后都进行了拆分,所以应该也能够实现陆续出现的效果。
小节
到此,热身结束,虽然上述两种实现方式都没有实现最终效果(其实是可以实现的),但是它们都给了我们一些非常重要的提示。css @keyframes提示我们需要复用动画函数,Vue transition提示我们需要定义合适的过渡状态,带着这两种想法那么接下来就进入揭晓谜底环节。
自定义动画
核心
自定义动画的核心就是上述的那两个提示:
- 可复用的动画函数
- 很合适的过渡状态
设计思路
首先,自定义动画肯定是一个class,那么class里面应该需要一些什么东西呢,一个普通的class里是有属性和方法,那么自定义动画class里肯定也有属性和方法。它的属性是什么,类比Vue transition,它的属性应该来自外部的配置参数,那么它的方法又应该是什么呢,没错,就是过渡状态函数以及动画启动函数。 Talk is cheap,show you my code:
class Transition {
constructor(dom, className, timeout, isDestory) {
this.dom = dom;
this.className = className;
this.timeout = timeout;
this.isDestory = isDestory;
this.preName = '';
}
startEnter() {
this.enter();
setTimeout(() => {
this.enterActive();
setTimeout(() => {
this.enterDone();
}, this.timeout);
}, this.timeout);
}
startExit() {
this.exit();
setTimeout(() => {
this.destory();
}, this.timeout);
}
destory() {
if (this.isDestory) {
this.dom.parentElement.removeChild(this.dom);
}
}
enter() {
this.preName = this.className + '-enter';
this.dom.classList.add(this.preName);
}
enterActive() {
this.dom.classList.remove(this.preName);
this.preName = this.className + '-enter-active';
this.dom.classList.add(this.preName);
}
enterDone() {
this.dom.classList.remove(this.preName);
this.preName = this.className + '-enter-done';
this.dom.classList.add(this.preName);
}
exit() {
if (this.preName !== '') {
this.dom.classList.remove(this.preName);
}
this.preName = this.className + '-exit';
this.dom.classList.add(this.preName);
}
}
这个Transition class就是依照目标动画效果而量身定制的,其中enter、enterActive、enterDone、exit以及destory都属于过渡状态函数,startEnter和startExit属于动画启动函数。 也许到这里,大家可能还感觉不到它的威力,那么接下我们开始使用它来改造代码。
重构列表动画
代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box2 {
width: 100%;
display: flex;
justify-content: center;
flex-wrap: wrap;
overflow: hidden;
margin-top: 100px;
}
.item {
width: 100%;
height: 16px;
font-family: PingFangSC-Regular;
text-align: center;
line-height: 16px;
color: #383838;
opacity: 0;
letter-spacing: 0;
margin-bottom: 15px;
}
.fade-enter {
opacity: 0;
line-height: 45px;
transform: scale(1);
}
.fade-enter-active {
opacity: 1;
line-height: 16px;
transform: scale(1.25);
transition: all ease-in 0.4s;
}
.fade-enter-done {
opacity: 0.6;
transition: all linear 0.4s;
}
.fade-exit {
opacity: 0;
transition: all ease-in 0.4s;
}
.move-exit {
margin-top: 70px;
transition: all ease-in 0.4s;
}
</style>
</head>
<body>
<div class="box2">
<div class="item">
<span>前端开发</span>
</div>
<div class="item">
<span>后端开发</span>
</div>
<div class="item">
<span>全栈开发</span>
</div>
</div>
<script>
class Transition {
}
const doms = document.getElementsByClassName('item');
const dom = document.getElementsByClassName('box2')[0];
const tl0 = new Transition(doms[0], 'fade', 400, true);
const tl1 = new Transition(doms[1], 'fade', 400, true);
const tl2 = new Transition(doms[2], 'fade', 400, true);
const tl = new Transition(dom, 'move', 400, true);
tl0.startEnter();
tl1.startEnter();
tl2.startEnter();
setTimeout(() => {
tl0.startExit();
tl1.startExit();
tl2.startExit();
tl.startExit();
}, 1300);
</script>
</body>
</html>
运行此代码,不难发现效果和第一种keyframes方案效果一模一样,细心的大家应该发现,在这里我们还重用了startExit这个动画启动函数,相比于keyframes方案而言提升了代码的复用性。 到此,还差最后一步,加入陆续呈现的效果,见证奇迹的时刻到了。
最终解决方案
正是因为我们将文字进入阶段和文字消失阶段拆解为两个动画启动函数,那么陆续启动不就是信手拈来。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box2 {
width: 100%;
display: flex;
justify-content: center;
flex-wrap: wrap;
overflow: hidden;
margin-top: 100px;
}
.item {
width: 100%;
height: 16px;
font-family: PingFangSC-Regular;
text-align: center;
line-height: 16px;
color: #383838;
opacity: 0;
letter-spacing: 0;
margin-bottom: 15px;
}
.fade-enter {
opacity: 0;
line-height: 45px;
transform: scale(1);
}
.fade-enter-active {
opacity: 1;
line-height: 16px;
transform: scale(1.25);
transition: all ease-in 0.4s;
}
.fade-enter-done {
opacity: 0.6;
transition: all linear 0.4s;
}
.fade-exit {
opacity: 0;
transition: all ease-in 0.4s;
}
.move-exit {
margin-top: 70px;
transition: all ease-in 0.4s;
}
</style>
</head>
<body>
<div class="box2">
<div class="item">
<span>前端开发</span>
</div>
<div class="item">
<span>后端开发</span>
</div>
<div class="item">
<span>全栈开发</span>
</div>
</div>
<script>
class Transition {
constructor(dom, className, timeout, isDestory) {
this.dom = dom;
this.className = className;
this.timeout = timeout;
this.isDestory = isDestory;
this.preName = '';
}
startEnter() {
this.enter();
setTimeout(() => {
this.enterActive();
setTimeout(() => {
this.enterDone();
}, this.timeout);
}, this.timeout);
}
startExit() {
this.exit();
setTimeout(() => {
this.destory();
}, this.timeout);
}
destory() {
if (this.isDestory) {
this.dom.parentElement.removeChild(this.dom);
}
}
enter() {
this.preName = this.className + '-enter';
this.dom.classList.add(this.preName);
}
enterActive() {
this.dom.classList.remove(this.preName);
this.preName = this.className + '-enter-active';
this.dom.classList.add(this.preName);
}
enterDone() {
this.dom.classList.remove(this.preName);
this.preName = this.className + '-enter-done';
this.dom.classList.add(this.preName);
}
exit() {
if (this.preName !== '') {
this.dom.classList.remove(this.preName);
}
this.preName = this.className + '-exit';
this.dom.classList.add(this.preName);
}
}
const tl0 = new Transition(doms[0], 'fade', 400, true);
const tl1 = new Transition(doms[1], 'fade', 400, true);
const tl2 = new Transition(doms[2], 'fade', 400, true);
const tl = new Transition(dom, 'move', 400, true);
tl0.startEnter();
setTimeout(() => {
tl1.startEnter();
setTimeout(() => {
tl2.startEnter();
setTimeout(() => {
tl0.startExit();
tl1.startExit();
tl2.startExit();
tl.startExit();
}, 1300);
}, 400);
}, 400);
</script>
</body>
</html>
运行此代码,呈现效果就和目标动画一模一样。 其实自定义动画的编写类似于重复造了一个Vue transition的轮子,只不过是简易版本的,但是却更实用。 这种方式虽然强大且万能,但是如果能够使用现有的轮子简单的实现了目标效果,那么当然也就没有必要花费大量时间精力再去重复造一个已经存在的一模一样的轮子。
结语
到此,本篇文章已临近尾声。这篇文章通过三种方式来实现一种动画效果让大家感受到每种方式的适用场景,从而让大家在编写动画时有更准确的判断和取舍。老生常谈,如果大家对本篇文章有任何疑问,欢迎评论区留言或者私信我,我会积极给与答复。送君千里终须一别,下期与君再会。
|