云表無代碼開發(fā)平臺baike seotl
這里給大家分享我在網(wǎng)上總結(jié)出來的一些知識,希望對大家有所幫助

本篇主要討論以下兩種翻書動畫的實現(xiàn):
第一種是整頁翻轉(zhuǎn)的效果:
這種整頁翻轉(zhuǎn)的效果主要是做rotateY的動畫,并結(jié)合一些CSS的3d屬性實現(xiàn)。
第二種折線翻轉(zhuǎn)的效果,如下圖所示:
主要是通過計算頁面翻折過來的位置。
這兩種原理上都不是很復(fù)雜,需要各個細(xì)節(jié)配合好,形成一個連貫的翻書動畫。
我們先重點說一下第一種翻頁效果的實現(xiàn)。
1. 基本布局
這種的實現(xiàn)相對比較簡單,我們先把DOM結(jié)構(gòu)準(zhǔn)備好,如下代碼所示:
<ul class="pages"><!--一個li.paper包含了正反兩頁--><li class="paper" data-left><!--一個.page就是一頁內(nèi)容--><div class="page page-1-back"><img src="1.jpg" alt></div><div class="page page-1"><img src="2.jpg" alt></div></li><li class="paper" data-right><div class="page page-2"><img src="3.jpg" alt></div><div class="page page-2-back"><img src="4.jpg" alt></div></li><!--其它頁內(nèi)容省略-->
</ul>
一個li.paper
就表示一張紙,包含了正反兩頁,data-left屬性表示它是在左邊的,而data-right表示是在右側(cè),通過absolute定位把它們放到相應(yīng)的位置。所以如果是下一頁,應(yīng)該讓data-right做左翻的動畫,相反上一頁則讓data-left做右翻的動畫。
.page-1是當(dāng)前顯示在左邊的那一頁,.page-2表示當(dāng)前右邊的那一頁,而.page-1-back和.page-2-back則分別表示在.paeg-1和.page-2后面的那一頁。它們置于背后是水平翻轉(zhuǎn)的,這一點應(yīng)該不難想象,所以需要借助transform: scale水平翻轉(zhuǎn)一下:
.page-1-back,
.page-2-back {transform: scale(-1, 1);
}
.page-1,
.page-2 {z-index: 1;
}
通過這樣排版之后,就得到了以下的布局:
接下來讓右邊的那一頁翻過來。
2. 翻書動畫
就是做.paper的rotateY動畫,很簡單,如下代碼所示:
@keyframes flip-to-left {from {transform: rotateY(0);}to {transform: rotateY(-180deg);}
}
.paper[data-right] {transform-origin: left center;animation: flip-to-left 2s ease-in-out;
}
需要設(shè)置變換中心為左邊中間的位置,效果如下:
?我們發(fā)現(xiàn)有幾個問題,第1個問題是翻過去的后面的那個paper沒有顯示出來,因為一開始把沒顯示出來的paper都隱藏了,所以需要把后面那個paper顯示出來:
.paper {display: none;position: absolute;/* 默認(rèn)放在右邊 */right: 0;
}
.paper[data-left],
.paper[data-right] {display: block;z-index: 1;
}
.paper[data-left] {right: auto;left: 0;
}
/* 把相鄰的paper顯示出來 */
.paper[data-right] + .paper {display: block;
}
這樣翻過來之后就能顯示后面的那個paper了,如下圖所示:
第二個問題是:為什么.page-2-back沒顯示出來,仍然顯示的是.page-2,猜測是因為.page-2的z-index比較高,把.page-2-back蓋住了,所以即使整體rotate屬性變了,它也是被蓋住的狀態(tài)。
所以第一個方法可以在翻轉(zhuǎn)一半的時候就把z-index的高低關(guān)系互換一下,讓page-2-back比page-2更高,但是這個方法不太好控制,因為動畫的變化不是linear的,即使是linear的這個方法也不靈活,容易出現(xiàn)閃動的情況。
第二個方法是調(diào)整它們倆的translateZ關(guān)系,讓page-2的translateZ值比page-2-back高1px就可以了,而不是直接設(shè)置z-index關(guān)系。為了讓translateZ能生效,需要設(shè)置它們?nèi)萜鞯?strong>transform-style為preserve-3d,如下代碼所示:
.paper {transform-style: preserve-3d;
}
.page-1,
.page-2 {transform: translateZ(1px);
}
?這樣基本的效果就出來了,但是總感覺哪里不太對,就是這個翻轉(zhuǎn)有點平,沒有景深的效果。說到景深會想起另外一個CSS屬性transform-perspective,我們不妨給它加一個perspective看看效果如何:
@keyframes flip-to-left {from {transform: perspective(1000px) rotateY(0);}to {transform: perspective(1000px) rotateY(-180deg);}
}
效果如下圖所示:
?這樣看起來感覺就立體多了。perspective可以理解為攝像機的位置,如果值越大攝像機就推得越遠。不同值對比如下:
這樣翻書的動畫基本就完成了,從左向右翻也是同樣道理。接下來的問題是怎么形成連續(xù)翻書的動畫。
3. 連續(xù)翻書
可以給動畫加一個forwards屬性,讓動畫保持在最后結(jié)束的那個狀態(tài):
.paper[data-right] {transform-origin: left center;animation: flip-to-left 2s ease-in-out forwards;
}
停住之后,上面那些類的關(guān)系需要重新更新一下,例如翻過來之后原本的.page-2-back會變成.page-1。
比較科學(xué)的方法是使用element.animate做動畫,因為它有一個onfinish的回調(diào)告訴我們動畫結(jié)束了,動畫由于這個API的兼容性不是很好,要么找個polyfill,要么還是用上面CSS的方法然后借助setTimeout。polyfill的庫比較大,這里還是用setTimeout模擬動畫結(jié)束,使用setTimeout的風(fēng)險是可能會不太準(zhǔn)。
代碼邏輯比較簡單,就是找到對應(yīng)的dom結(jié)點設(shè)置對應(yīng)的類或者屬性,就是代碼比較繁瑣一點,如下所示:
let currentPage = 1;
let $ = document.querySelector.bind(document);
$('#next-page').addEventListener('click', goToNextPage);
const flipAnimateTime = 1000;
function goToNextPage () {// 觸發(fā)CSS動畫$('.paper[data-right]').setAttribute('data-begin-animate', true);setTimeout(() => {// data-right變成data-leftlet $rightPaper = $('.paper[data-right]'),$leftPaper = $('.paper[data-left]');$rightPaper.removeAttribute('data-right');$rightPaper.setAttribute('data-left', true);// data-left沒有了$leftPaper.removeAttribute('data-left');$leftPaper.querySelector('.page-1').classList.remove('page-1');$leftPaper.querySelector('.page-1-back').classList.remove('page-1-back');// 重新設(shè)置類的關(guān)系$leftPaper = $rightPaper;$rightPaper = $leftPaper.nextElementSibling;$leftPaper.querySelector('.page').classList.add('page-1-back');$leftPaper.querySelector('.page + .page').classList.add('page-1');// 如果還有下一頁if ($rightPaper) {$rightPaper.setAttribute('data-right', true);$rightPaper.querySelector('.page').classList.add('page-2');$rightPaper.querySelector('.page + .page').classList.add('page-2-back');} currentPage++;}, flipAnimateTime);
}
向左翻頁也是類似。
這里有個問題,如果用戶點下一頁點得很快那應(yīng)該怎么辦?如果他點得很快的話前面的翻頁還沒有結(jié)束,會導(dǎo)致setTimeout里的代碼還沒有執(zhí)行,那么整個模型就亂了。有兩個解決方法,第一種是在翻頁過程中禁掉下一頁的操作,但是似乎不太友好,第二種是把翻頁的過程當(dāng)作一個task任務(wù),一旦點了下一頁或者上一頁,就push一個task進來,每個task按順序同步執(zhí)行,如果task數(shù)組長度大于1那么就縮短動畫的時間,讓它翻得快一點。相似的處理我已經(jīng)在《實現(xiàn)內(nèi)部組件輪播切換效果》討論過,這里不再重復(fù)。
4. 適配的問題
你可能會擔(dān)心動畫結(jié)束后修改了dom結(jié)構(gòu),導(dǎo)致CSS屬性變了會閃一下,例如原本的page-2-back是水平翻轉(zhuǎn)的,但是在JS里面設(shè)置了之后它就變成非水平翻轉(zhuǎn)了,雖然展示的效果是一樣的,但是會不會閃一下呢?只要改之前和改之后瀏覽器進行l(wèi)ayout計算的結(jié)果一模一樣它就不會閃的,就像上面的例子,但是一旦位移差了1px,就會有閃動。
在實際的例子,你可能需要中間有1px的書縫的陰影,所以左右頁的寬度就不是剛好50%,而是要減去1px,所以如果你的transform-origin還是left center的話翻過去之后就會往右移了1px,當(dāng)動畫結(jié)束重置狀態(tài),1px的偏移就會被修正,這個時候就會小閃一下。而當(dāng)你把transform-origin改成-1px center之后,又會導(dǎo)致翻過去之后往左移了1px。所以最好別把中間的陰影單獨弄出來,可以改成在每一個page里面用before/after畫,每個page還是要占50%,這樣就沒問題。
另外一個要考慮的問題是,使用了transform: scale + translateZ可能會導(dǎo)致模糊,一個直接的例子可以見這個codepen,就是因為用了translateZ或者will-change: transform等觸發(fā)了GPU渲染導(dǎo)致模糊了,這個過程可能是瀏覽器把當(dāng)前圖層截一張圖給GPU計算,GPU把這張靜態(tài)圖縮放就會模糊。而當(dāng)我們把translateZ等有promotion提升作用的屬性去掉之后,在縮放的過程會模糊,但是最終狀態(tài)是清晰的。如下圖所示:
5. 變成一個插件
當(dāng)把上面的問題都解決了之后,可以把它變成一個插件,用的人只要引入,然后初始化一下就搞定了,不用關(guān)心這些類怎么變之類的問題。
并且,由于一個paper容器有兩個page是正反面的關(guān)系,一旦中間突然插入了一頁就會導(dǎo)致page的正反面關(guān)系發(fā)生變化,所以這個結(jié)構(gòu)不是很靈活,最好是動態(tài)生成,也就是說使用插件的人,把所有的page并列排就好了,然后在插件里面再重新組織下DOM結(jié)構(gòu),把在正反面的兩個page放到一個paper里面。
接著討論下第二種翻書效果的實現(xiàn)。
6. 折線翻書效果的實現(xiàn)
這個有一個現(xiàn)成的插件turn.js,使用起來非常簡單,我們簡單討論一下它的內(nèi)部實現(xiàn)。
這個東西乍看一下,似乎有曲面的效果:
?但實際上是沒有的,這個曲線效果是它添加的陰影和漸變產(chǎn)生的視覺效果,當(dāng)我們把background-image的漸變?nèi)サ糁髮Ρ纫幌戮湍芸闯鰜砹?#xff1a;
?沒有漸變的偽裝之后一下子就平了。它就變成了一個折紙的模型——給定一張紙和一個折過去的點,計算一下折過去的旋轉(zhuǎn)角度和位移。它的源代碼是在fold函數(shù)里面計算的:
它里面有各種余弦正弦的計算和角度的判斷,具體實現(xiàn)還是比較復(fù)雜的,沒有深入去研究,代碼可見turn.js.
還有一個問題是它是怎么實現(xiàn)三角形裁剪展示的效果?它是在上層又蓋了一個div:
7. 小結(jié)
本文討論了兩種翻書效果的實現(xiàn),重點討論了一下比較簡單的整體翻頁的方式,這種方式主要是做rotateY動畫,同時打開perspective讓其具有景深效果,并且用preserve-3d結(jié)合translateZ制造上下層級關(guān)系,這種方式可能會存在閃動和模糊的問題,為了讓翻過去不會閃動關(guān)鍵的地方是保證每一個page占寬50%,模糊的問題是因為用了scale加上GPU提升導(dǎo)致的,所以只能通過不寫3d屬性保證清晰。
第二種的效果模型相對比較復(fù)雜,簡單分析了一下它的原理和實現(xiàn)方式。主要是計算折紙過來的角度和位移,上層再蓋一個div隱藏不露出來的部分。然后再加上陰影和漸變制造一種曲面的效果。這種翻書的效果還是挺好玩的。
本文轉(zhuǎn)載于:
https://juejin.cn/post/6844903665216520206
如果對您有所幫助,歡迎您點個關(guān)注,我會定時更新技術(shù)文檔,大家一起討論學(xué)習(xí),一起進步。
