【太直观了!】三维模型手把手教你做翻盘器,不只是时钟

454 阅读6分钟

前言

公司动不动就搞活动撒钱🎉🧧,为了激起用户的欲望,还要搞个倒计时⏰,Tick~ Tick~ Tick~,搞了几次活动后,我准备把翻牌效果的实现记录下来。

障眼法

首先给大家看一张图,这张图上有一个数字6,看起来平淡无奇,没什么特别的。

不识庐山真面目

然而,当我换一个角度,你才得以窥见了它的原貌。

只缘身在此山中
原来,你看见的 6 是由上下两部分组成的。 绿色的卡片上有一个白色的数字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

整个过程大概就是这样,当然,除了更新数字,你也可以更新汉字,或者其它符号。

你可以在这获取更直观的体验,3D效果

Github地址

第一次在掘金发文,大佬们多多指点。