前言
公司动不动就搞活动撒钱🎉🧧,为了激起用户的欲望,还要搞个倒计时⏰,Tick~ Tick~ Tick~,搞了几次活动后,我准备把翻牌效果的实现记录下来。
障眼法
首先给大家看一张图,这张图上有一个数字6,看起来平淡无奇,没什么特别的。
然而,当我换一个角度,你才得以窥见了它的原貌。
而且,图上还有一个隐藏的数字 5,5也是有两个卡片组成,一前一后,最靠近你的那个 5(这是一个下半部分的5),垂直于屏幕,所以你是看不见的,而最后面的那个5,被6挡住了,你同样看不见。
翻转的障眼法
然后我们再来看一个动画,我手动将 6 翻转90度,然后,紧接着,我再把最靠近你的 5 也旋转90度。最后我们再看一眼效果,Woow~
原理
原来背后的魔法是这样的:最开始的时候,我们看到的6,是由上下两部分组成,一部分在前面,一部分在后面。当6的上半部分往下旋转的时候,最后面的5会慢慢漏出来,当6的上半部分旋转到90度的时候,停止。紧接着,需要数字5来接力。数字5接着往下旋转,在它旋转的过程中,6的下半部分被慢慢挡住,进而产生了翻牌的效果。
瞬间复位
然而,这就完了吗?显然没有,我们前面的演示,只是解释了从6过度到5的过程,那从5过度到4呢?这就是我接下来要说的另一个浏览器的障眼法,姑且叫他「瞬间复位」吧。具体什么意思呢?我们再来看一个动画吧~
说时迟,那时快,当从6翻牌到5完成之后,瞬间,Suddenly!5的上半部分占据了之前6的上半部分的位置,同时5的另一部分占据了最后面的6的位置,同时,下一个数字4,占据了之前数字5的部分,整个过程一气呵成,你根本察觉不到。
编码实现
好了,了解了翻牌的整个过程原理之后,编码实现就容易的多了,首先,我们需要4个元素,分别表示上图中的4个数字。
HTML部分
相对简单,不多解释
<div class="container">
<!-- 当前数字,上半部分 -->
<div class="curr-upper"></div>
<!-- 下一个数字,下半部分 -->
<div class="next-lower"></div>
<!-- 当前数字,下半部分 -->
<div class="curr-lower"></div>
<!-- 下一个数字,下半部分 -->
<div class="next-upper"></div>
</div>
CSS 部分
元素的前后关系和旋转效果主要通过 transform 的 translateZ 和 rotate来实现。 包裹四个数字的container为relative定位,四个数字为position定位。
如何只显示元素的上半部分
假设container的高200px,那么他一半的高就是100px,同时设置数字的line-height 为200px,此时数字的垂直居中线刚好位于其元素的最底边,超出的部分隐藏(overflow:hidden),然后我们就得到了只显示数字上半部分的效果。 具体写法可以参看这里
如何只显示元素的下半部分
思考:当数字的垂直居中线位于其元素的最顶边时,数字的下半部分将会显示在元素中。其实也就是其line-height 为 0; 具体写法可以参看这里
.container {
margin: 200px auto;
position: relative;
width: 200px;
height: 200px;
background: red;
text-align: center;
perspective: 800px;
transform: rotateY(0deg);
perspective-origin: 50% 50%;
transform-style: preserve-3d;
}
.container > div {
position: absolute;
width: 100%;
height: 50%;
font-size: 150px;
overflow: hidden;
color: #fff;
}
.curr-upper {
line-height: 200px;
transform: translate3d(0, 0, 300px) rotateX(0);
transform-origin: center bottom;
background: skyblue;
}
.curr-lower {
bottom: 0;
line-height: 0;
transform: translate3d(0, 0, 200px);
background: yellow;
}
.curr-upper.active {
transform: translate3d(0, 0, 300px) rotateX(-90deg);
}
.next-lower {
bottom: 0;
line-height: 0;
transform: translate3d(0, 0, 300px) rotateX(90deg);
transform-origin: center top;
background: pink;
}
.next-lower.active {
transform: translate3d(0, 0, 300px) rotateX(0deg);
}
.next-upper {
width: 100%;
height: 100%;
line-height: 200px;
transform: translate3d(0, 0, 100px);
background: blue;
}
JavaScript部分
首先说一下调用方式,我这里是需要先new一个翻牌器的实例,然后手动调用其实例的update方法,每调用一次update方法,翻牌器就会更新一次,至于多长时间更新一次,一秒还是两秒,还是一分钟,交给调用者去决定。
let n = 9;
const flipper = new Flipper(n)
setInterval(() => {
n--;
flipper.update(n)
}, 3000)
首先我们声明一个 Flipper 类,在其构造函数内,我们获取到这4个数字对应的元素。
class Flipper {
/**
* 构造函数
* @param {number} initVal 初始值
*/
constructor(initVal) {
// 当前数字的上半部分
this.currUpper = document.querySelector('.curr-upper');
// 当前数字的下半部分
this.currLower = document.querySelector('.curr-lower');
// 下一个数字的下半部分,由于该元素在最后面,为了简单,css里并没有将其处理为一半,而是保留了整体的高度
this.nextLower = document.querySelector('.next-lower');
// 下一个数字的上半部分
this.nextUpper = document.querySelector('.next-upper');
// 然后将初始值赋值给当前数字,并且保存一份到 this.currVal 上
this.currUpper.innerText = this.currLower.innerText = this.currVal = initVal
// 记录当前是否正在进行翻牌动画
this.animating = false
}
}
然后添加update方法,update时,会判断下一个数字是否等于当前的数字,如果相等,则不会更新。或者如果上一次的翻盘动画还没有执行完,同样也不会更新。
update(nextVal) {
// 上一次的翻牌动画还没有执行完
if (this.animating) return
// 要更新的数字和上一个数字相等
if (nextVal === this.currVal) return
// 把下一个要更新的数字,先更新到dom中。
this.nextLower.innerText = this.nextUpper.innerText = this.nextVal = nextVal
// 然后开始翻牌动画,就是我们演示的过程
this.animate()
}
翻牌动画方法
animate() {
// 首先立个flag 动画正在进行时
this.animating = true
// 然后给接下来要旋转的那两个元素分别加上transion属性
// 假设当前数字旋转 90 度需要0.2s完成
// 那么下一个数字要延迟0.2s再开始执行动画,这是一场接力赛
this.currUpper.style.transition = 'transform 0.2s ease';
this.nextLower.style.transition = 'transform 0.2s ease 0.2s';
// 给这两个元素添加active类,动画开始了
this.currUpper.classList.add('active')
this.nextLower.classList.add('active')
// 由于整个接力旋转总共需要耗时0.4s,所以,等到0.4s后,我们开始瞬间复位
setTimeout(() => {
this.reset()
}, 400)
}
瞬间复位方法
// 不要看到我们下面做了这么多操作,其实整个过程是在一瞬间完成的
reset() {
// 当前值变为下一个值
this.currVal = this.nextVal
// 同时,当前数字对应的元素也要更新为下一个值
this.currUpper.innerText = this.currLower.innerText = this.currVal
// 这个过程不需要动画,所以把transition属性去掉
this.currUpper.style.transition = ''
this.nextLower.style.transition = ''
// 分别去掉active类
this.currUpper.classList.remove('active')
this.nextLower.classList.remove('active')
// flag置为false
this.animating = false
}
End
整个过程大概就是这样,当然,除了更新数字,你也可以更新汉字,或者其它符号。