翻书动画组件(vue2 + sass)
前言
之前在网上看到一个翻书动画的实现,觉得非常巧妙,借此机会记录练习一下。
前端框架 vue 提供了模板式开发,利用 vue 语法能更方便地创造和操作 dom,而且非常方便与 css 预处理语言 sass 集成,后期也非常方便地与实际项目进行集成。
此外项目还对移动端进行了适配,根据不同屏幕尺寸大小进行调整。
准备工作
1.使用 vue-cli 3.0 创建一个 vue 项目。
2.在创建完成的项目下导入素材到 src/assets/images
第一部分
翻书动画,先来看看成品效果:
翻书动画.gif分析一下布局:
我们定义了一个 flapCardList 对象存储了所有卡片的背景图片和背景 r,g,b 值,还有旋转的角度等:
export const flapCardList = [
{
r: 255,
g: 102,
_g: 102,
b: 159,
imgLeft: 'url(' + require('@/assets/images/gift-left.png') + ')',
imgRight: 'url(' + require('@/assets/images/gift-right.png') + ')',
backgroundSize: '50% 50%',
zIndex: 100,
rotateDegree: 0
},
···
]
这样我们就可以根据 flapCardList 的内容来循环创建所有动画卡片的 dom 并且动态控制。直接看这个布局结构,逐层嵌套,很好理解,重点是如何靠 css 来实现实际的效果。由于采用了 vue + scss 的方案,我们可以把公共的布局抽象出来便于调用。具体的做法是使用 scss 的 mixin 机制,这块大家可以在源码中具体去观察下做法。接下里的步骤里会逐步分析实现过程,同时也会包含 css 的控制。我们先简单地预览下完整的 dom 的结构,给左右两个小卡片绑定了 css 设置方法 semiCircleStyle,用来设置背景图片,背景颜色,尺寸。
<template>
<div class="flap-card-wrapper">
<div class="flap-card-bg">
<div class="flap-card" v-for="(item, index) in flapCardList" :key="index" :style="{zIndex: item.zIndex}">
<div class="flap-card-circle">
<div class="flap-card-semi-circle flap-card-semi-circle-left" :style="semiCircleStyle(item, 'left')" ref="left">
</div>
<div class="flap-card-semi-circle flap-card-semi-circle-right" :style="semiCircleStyle(item, 'right')" ref="right">
</div>
</div>
</div>
</div>
</div>
</template>
接着分析动画的过程,有别于 canvs api 的绘图,css 呈现的效果只是 2D 的平面效果,没法做到如 three.js, ht.js 等基于 webGL 库所绘制出来的真实 3D 场景,因而我们永远需要去控制显示的层级才能实现效果,为什么说这个呢,因为我们的卡片翻转只能从正面看,因而当前面卡片翻转后需要使后面卡片的 z-index 属性大于翻转过的卡片才能看到。
观察上面的 gif 图片我们尝试总结一下动画的步骤:
1.前面的卡片翻转,前面的卡片转动的角度到达 90 度的时候隐藏。
2.这个时候,左边部分显示背面的转动。
3.当转动角度达到 180 度时,一个卡片翻转完,继续下一张卡片的翻转。
这么分析下来其实并不知道该如何下手,我们试试想下现在能做什么,先写一个函数让第一个卡片动起来吧。
先说明一下 rotateY 这个属性,与 rotateX 和 rotateZ 不同,rotateY 正方向为逆时针,在网页里就是从右边向屏幕这个方向,反之亦然。
旋转本质上是改变对象的 rotateY,通过定时器进行值的变化:
rotate(index, type) {
let item = this.flapCardList[index ]
let dom = type === 'front' ? this.$refs.right[index] : this.$refs.left[index]
dom.style.transform = `rotateY(${item.rotateDegree}deg)`
dom.style.backgroundColor = `rgb(${item.r}, ${item._g}, ${item.b})`
},
startFlapAnimation() {
setInterval(() => {
const frontFlapCard = this.flapCardList[this.front]
frontFlapCard.rotateDegree += 10
this.rotate(0, 'right')
}, this.flapInterval)
}
在 mounted 里开启动画:
mounted() {
this.startFlapAnimation()
}
效果预览:
1.gif旋转没有绕着中心轴,添加 css 属性:
.flap-card-semi-circle-left {
border-radius: px2rem(24) 0 0 px2rem(24);
background-position: center right;
transform-origin: right;
}
.flap-card-semi-circle-right {
border-radius: 0 px2rem(24) px2rem(24) 0;
background-position: center left;
transform-origin: left;
}
运行起来发现有有两个问题,一个是正面转过 90 度的时候没有隐藏,一个是背面旋转。
隐藏可以使用设置背面的时候隐藏。
.flap-card-semi-circle {
flex: 0 0 50%;
width: 50%;
height: 100%;
background-repeat: no-repeat;
backface-visibility: hidden;
}
这个时候再运行就可以了。
这时如果像第一张卡片旋转这样去设置背面卡片的运动是无法显示的,因为背面是左边转动,由于 z-index 比较小,所以会永远被左边的覆盖。
这个时候就需要在临界值转过 90 度的时候改变背面卡片的 z-index 值,正面卡片转过 90 度, 背面卡片也需要跟着转,这时就需要在转动之前先让背面卡片转过 180 度与右半部分重合,然后在反方向旋转,当转过 90 度的时候就与正面卡片隔着屏幕对称了,这个时候继续运动就可以得到渐入的效果, 逐渐覆盖前面的卡片,具体的实现如下:
// 下一张卡片动作前先做准备
prepare() {
const backFlapCard = this.flapCardList[this.back]
backFlapCard.rotateDegree = 180
this.rotate(this.back, 'back')
},
startFlapAnimation() {
// 开始就需要先预制一次
this.prepare()
setInterval(() => {
const frontFlapCard = this.flapCardList[this.front]
const backFlapCard = this.flapCardList[this.back]
// this.prepare()
frontFlapCard.rotateDegree += 10
backFlapCard.rotateDegree -= 10
if (frontFlapCard.rotateDegree === 90 && backFlapCard.rotateDegree === 90) {
backFlapCard.zIndex += 2
}
this.rotate(this.front, 'front')
this.rotate(this.back, 'back')
}, this.flapInterval)
预览下效果:
2.gif
接下来需要把所有的卡片加入循环,除了需要 this.front || back 这两个计数需要增加,我们还需要做几个操作:
1.把已经旋转过的卡片角度设置为原来的角度,也就是 0;
2.旋转 180 度进行一次切换,z-index 需要轮换:
100 = 96
99 = 97
98 = 96
97 = 99
96 = 100
具体实现:
next() {
// 重置状态
const frontFlapCard = this.flapCardList[this.front]
const backFlapCard = this.flapCardList[this.back]
frontFlapCard.rotateDegree = 0
backFlapCard.rotateDegree = 0
this.rotate(this.front, 'front')
this.rotate(this.back, 'back')
// 计数增加
const len = this.flapCardList.length
this.front++
this.back++
if (this.front >= len) {
this.front = 0
}
if (this.back >= len) {
this.back = 0
}
// 轮换 zIndex
this.flapCardList.forEach((item, index) => {
item.zIndex = 100 - ((index - this.front + len) % len)
})
this.prepare()
},
startFlapAnimation() {
// 开始就需要先预制一次
this.prepare()
setInterval(() => {
const frontFlapCard = this.flapCardList[this.front]
const backFlapCard = this.flapCardList[this.back]
frontFlapCard.rotateDegree += 10
backFlapCard.rotateDegree -= 10
if (frontFlapCard.rotateDegree === 90 && backFlapCard.rotateDegree === 90) {
backFlapCard.zIndex += 2
}
this.rotate(this.front, 'front')
this.rotate(this.back, 'back')
if (frontFlapCard.rotateDegree === 180 && backFlapCard.rotateDegree === 0) {
this.next()
}
}, this.flapInterval)
}
再看下效果:
3.gif
可以看到效果基本实现,这里还可以进一步地优化下效果,在前面卡片转动的过程中颜色逐渐加深,后面卡片转动的时候颜色逐渐变浅,这个做法也很简单,通过动态地设置 backgroundColor 的 _g 分量值。需要注意的是背面卡片提前转过了 180 度,直到背面卡片到 0 的时候转过了 18 次,需要提前加深响应的颜色:
prepare() {
const backFlapCard = this.flapCardList[this.back]
backFlapCard.rotateDegree = 180
this.rotate(this.back, 'back')
backFlapCard._g = backFlapCard.g + 5 * 18
}
然后在 next 函数里把之前的颜色重置:
frontFlapCard._g = frontFlapCard.g
backFlapCard._g = backFlapCard.g
至此一个基本的效果就完成了。