前言
之前在網(wǎng)上看到一個翻書動畫的實現(xiàn)绪励,覺得非常巧妙陕悬,借此機會記錄練習(xí)一下灌诅。
前端框架 vue 提供了模板式開發(fā),利用 vue 語法能更方便地創(chuàng)造和操作 dom单刁,而且非常方便與 css 預(yù)處理語言 sass 集成灸异,后期也非常方便地與實際項目進行集成府适。
此外項目還對移動端進行了適配,根據(jù)不同屏幕尺寸大小進行調(diào)整肺樟。
準備工作
1.使用 vue-cli 3.0 創(chuàng)建一個 vue 項目檐春。
2.在創(chuàng)建完成的項目下導(dǎo)入素材到 src/assets/images
第一部分
翻書動畫,先來看看成品效果:
分析一下布局:
我們定義了一個 flapCardList 對象存儲了所有卡片的背景圖片和背景 r,g,b 值么伯,還有旋轉(zhuǎn)的角度等:
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
},
···
]
這樣我們就可以根據(jù) flapCardList 的內(nèi)容來循環(huán)創(chuàng)建所有動畫卡片的 dom 并且動態(tài)控制疟暖。直接看這個布局結(jié)構(gòu),逐層嵌套田柔,很好理解俐巴,重點是如何靠 css 來實現(xiàn)實際的效果。由于采用了 vue + scss 的方案硬爆,我們可以把公共的布局抽象出來便于調(diào)用欣舵。具體的做法是使用 scss 的 mixin 機制,這塊大家可以在源碼中具體去觀察下做法缀磕。接下里的步驟里會逐步分析實現(xiàn)過程缘圈,同時也會包含 css 的控制。我們先簡單地預(yù)覽下完整的 dom 的結(jié)構(gòu)袜蚕,給左右兩個小卡片綁定了 css 設(shè)置方法 semiCircleStyle糟把,用來設(shè)置背景圖片,背景顏色廷没,尺寸糊饱。
<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 呈現(xiàn)的效果只是 2D 的平面效果另锋,沒法做到如 three.js, ht.js 等基于 webGL 庫所繪制出來的真實 3D 場景,因而我們永遠需要去控制顯示的層級才能實現(xiàn)效果狭归,為什么說這個呢夭坪,因為我們的卡片翻轉(zhuǎn)只能從正面看,因而當前面卡片翻轉(zhuǎn)后需要使后面卡片的 z-index 屬性大于翻轉(zhuǎn)過的卡片才能看到过椎。
觀察上面的 gif 圖片我們嘗試總結(jié)一下動畫的步驟:
1.前面的卡片翻轉(zhuǎn)室梅,前面的卡片轉(zhuǎn)動的角度到達 90 度的時候隱藏。
2.這個時候疚宇,左邊部分顯示背面的轉(zhuǎn)動亡鼠。
3.當轉(zhuǎn)動角度達到 180 度時,一個卡片翻轉(zhuǎn)完敷待,繼續(xù)下一張卡片的翻轉(zhuǎn)间涵。
這么分析下來其實并不知道該如何下手,我們試試想下現(xiàn)在能做什么榜揖,先寫一個函數(shù)讓第一個卡片動起來吧勾哩。
先說明一下 rotateY 這個屬性抗蠢,與 rotateX 和 rotateZ 不同,rotateY 正方向為逆時針思劳,在網(wǎng)頁里就是從右邊向屏幕這個方向迅矛,反之亦然。
旋轉(zhuǎn)本質(zhì)上是改變對象的 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()
}
效果預(yù)覽:
旋轉(zhuǎn)沒有繞著中心軸秽褒,添加 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;
}
運行起來發(fā)現(xiàn)有有兩個問題,一個是正面轉(zhuǎn)過 90 度的時候沒有隱藏威兜,一個是背面旋轉(zhuǎn)震嫉。
隱藏可以使用設(shè)置背面的時候隱藏。
.flap-card-semi-circle {
flex: 0 0 50%;
width: 50%;
height: 100%;
background-repeat: no-repeat;
backface-visibility: hidden;
}
這個時候再運行就可以了牡属。
這時如果像第一張卡片旋轉(zhuǎn)這樣去設(shè)置背面卡片的運動是無法顯示的,因為背面是左邊轉(zhuǎn)動扼睬,由于 z-index 比較小逮栅,所以會永遠被左邊的覆蓋。
這個時候就需要在臨界值轉(zhuǎn)過 90 度的時候改變背面卡片的 z-index 值窗宇,正面卡片轉(zhuǎn)過 90 度措伐, 背面卡片也需要跟著轉(zhuǎn),這時就需要在轉(zhuǎn)動之前先讓背面卡片轉(zhuǎn)過 180 度與右半部分重合军俊,然后在反方向旋轉(zhuǎn)侥加,當轉(zhuǎn)過 90 度的時候就與正面卡片隔著屏幕對稱了,這個時候繼續(xù)運動就可以得到漸入的效果粪躬, 逐漸覆蓋前面的卡片担败,具體的實現(xiàn)如下:
// 下一張卡片動作前先做準備
prepare() {
const backFlapCard = this.flapCardList[this.back]
backFlapCard.rotateDegree = 180
this.rotate(this.back, 'back')
},
startFlapAnimation() {
// 開始就需要先預(yù)制一次
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)
預(yù)覽下效果:
接下來需要把所有的卡片加入循環(huán),除了需要 this.front || back 這兩個計數(shù)需要增加镰官,我們還需要做幾個操作:
1.把已經(jīng)旋轉(zhuǎn)過的卡片角度設(shè)置為原來的角度提前,也就是 0;
2.旋轉(zhuǎn) 180 度進行一次切換泳唠,z-index 需要輪換:
100 = 96
99 = 97
98 = 96
97 = 99
96 = 100
具體實現(xiàn):
next() {
// 重置狀態(tài)
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')
// 計數(shù)增加
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() {
// 開始就需要先預(yù)制一次
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)
}
再看下效果:
可以看到效果基本實現(xiàn)狈网,這里還可以進一步地優(yōu)化下效果,在前面卡片轉(zhuǎn)動的過程中顏色逐漸加深笨腥,后面卡片轉(zhuǎn)動的時候顏色逐漸變淺拓哺,這個做法也很簡單,通過動態(tài)地設(shè)置 backgroundColor 的 _g 分量值脖母。需要注意的是背面卡片提前轉(zhuǎn)過了 180 度士鸥,直到背面卡片到 0 的時候轉(zhuǎn)過了 18 次,需要提前加深響應(yīng)的顏色:
prepare() {
const backFlapCard = this.flapCardList[this.back]
backFlapCard.rotateDegree = 180
this.rotate(this.back, 'back')
backFlapCard._g = backFlapCard.g + 5 * 18
}
然后在 next 函數(shù)里把之前的顏色重置:
frontFlapCard._g = frontFlapCard.g
backFlapCard._g = backFlapCard.g
至此一個基本的效果就完成了镶奉。