本篇主要討論如下兩種翻書動畫的實現:javascript
第一種是整頁翻轉的效果:css
這種整頁翻轉的效果主要是作rotateY的動畫,並結合一些CSS的3d屬性實現。html
第二種折線翻轉的效果,以下圖所示:vue
主要是經過計算頁面翻折過來的位置。java
這兩種原理上都不是很複雜,須要各個細節配合好,造成一個連貫的翻書動畫。git
咱們先重點說一下第一種翻頁效果的實現。github
這種的實現相對比較簡單,咱們先把DOM結構準備好,以下代碼所示:web
<ul class="pages"> <!--一個li.paper包含了正反兩頁--> <li class="paper" data-left> <!--一個.page就是一頁內容--> <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> <!--其它頁內容省略--> </ul>複製代碼
一個li.paper
就表示一張紙,包含了正反兩頁,data-left屬性表示它是在左邊的,而data-right表示是在右側,經過absolute定位把它們放到相應的位置。因此若是是下一頁,應該讓data-right作左翻的動畫,相反上一頁則讓data-left作右翻的動畫。小程序
.page-1是當前顯示在左邊的那一頁,.page-2表示當前右邊的那一頁,而.page-1-back和.page-2-back則分別表示在.paeg-1和.page-2後面的那一頁。它們置於背後是水平翻轉的,這一點應該不難想象,因此須要藉助transform: scale水平翻轉一下:windows
.page-1-back,
.page-2-back {
transform: scale(-1, 1);
}複製代碼
而且.page-1的z-index要比在後面的.page-1-back要高:
.page-1,
.page-2 {
z-index: 1;
}複製代碼
經過這樣排版以後,就獲得瞭如下的佈局:
接下來讓右邊的那一頁翻過來。
就是作.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;
}複製代碼
須要設置變換中心爲左邊中間的位置,效果以下:
咱們發現有幾個問題,第1個問題是翻過去的後面的那個paper沒有顯示出來,由於一開始把沒顯示出來的paper都隱藏了,因此須要把後面那個paper顯示出來:
.paper {
display: none;
position: absolute;
/* 默認放在右邊 */
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屬性變了,它也是被蓋住的狀態。
因此第一個方法能夠在翻轉一半的時候就把z-index的高低關係互換一下,讓page-2-back比page-2更高,可是這個方法不太好控制,由於動畫的變化不是linear的,即便是linear的這個方法也不靈活,容易出現閃動的狀況。
第二個方法是調整它們倆的translateZ關係,讓page-2的translateZ值比page-2-back高1px就能夠了,而不是直接設置z-index關係。爲了讓translateZ能生效,須要設置它們容器的transform-style爲preserve-3d,以下代碼所示:
.paper {
transform-style: preserve-3d;
}
.page-1,
.page-2 {
transform: translateZ(1px);
}複製代碼
這個可讓子元素從扁平空間(flat)變成一個3維空間,translateZ就能發揮做用了,效果以下所示:
這樣基本的效果就出來了,可是總感受哪裏不太對,就是這個翻轉有點平,沒有景深的效果。說到景深會想起另一個CSS屬性transform-perspective,咱們不妨給它加一個perspective看看效果如何:
@keyframes flip-to-left {
from {
transform: perspective(1000px) rotateY(0);
}
to {
transform: perspective(1000px) rotateY(-180deg);
}
}複製代碼
效果以下圖所示:
這樣看起來感受就立體多了。perspective能夠理解爲攝像機的位置,若是值越大攝像機就推得越遠。不一樣值對好比下:
這樣翻書的動畫基本就完成了,從左向右翻也是一樣道理。接下來的問題是怎麼造成連續翻書的動畫。
能夠給動畫加一個forwards屬性,讓動畫保持在最後結束的那個狀態:
.paper[data-right] {
transform-origin: left center;
animation: flip-to-left 2s ease-in-out forwards;
}複製代碼
停住以後,上面那些類的關係須要從新更新一下,例如翻過來以後本來的.page-2-back會變成.page-1。
比較科學的方法是使用element.animate作動畫,由於它有一個onfinish的回調告訴咱們動畫結束了,動畫因爲這個API的兼容性不是很好,要麼找個polyfill,要麼仍是用上面CSS的方法而後藉助setTimeout。polyfill的庫比較大,這裏仍是用setTimeout模擬動畫結束,使用setTimeout的風險是可能會不太準。
代碼邏輯比較簡單,就是找到對應的dom結點設置對應的類或者屬性,就是代碼比較繁瑣一點,以下所示:
let currentPage = 1;
let $ = document.querySelector.bind(document);
$('#next-page').addEventListener('click', goToNextPage);
const flipAnimateTime = 1000;
function goToNextPage () {
// 觸發CSS動畫
$('.paper[data-right]').setAttribute('data-begin-animate', true);
setTimeout(() => {
// data-right變成data-left
let $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');
// 從新設置類的關係
$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);
}複製代碼
效果以下圖所示:
向左翻頁也是相似。
這裏有個問題,若是用戶點下一頁點得很快那應該怎麼辦?若是他點得很快的話前面的翻頁尚未結束,會致使setTimeout裏的代碼尚未執行,那麼整個模型就亂了。有兩個解決方法,第一種是在翻頁過程當中禁掉下一頁的操做,可是彷佛不太友好,第二種是把翻頁的過程看成一個task任務,一旦點了下一頁或者上一頁,就push一個task進來,每一個task按順序同步執行,若是task數組長度大於1那麼就縮短動畫的時間,讓它翻得快一點。類似的處理我已經在《實現內部組件輪播切換效果》討論過,這裏再也不重複。
你可能會擔憂動畫結束後修改了dom結構,致使CSS屬性變了會閃一下,例如本來的page-2-back是水平翻轉的,可是在JS裏面設置了以後它就變成非水平翻轉了,雖然展現的效果是同樣的,可是會不會閃一下呢?只要改以前和改以後瀏覽器進行layout計算的結果如出一轍它就不會閃的,就像上面的例子,可是一旦位移差了1px,就會有閃動。
在實際的例子,你可能須要中間有1px的書縫的陰影,因此左右頁的寬度就不是恰好50%,而是要減去1px,因此若是你的transform-origin仍是left center的話翻過去以後就會往右移了1px,當動畫結束重置狀態,1px的偏移就會被修正,這個時候就會小閃一下。而當你把transform-origin改爲-1px center以後,又會致使翻過去以後往左移了1px。因此最好別把中間的陰影單獨弄出來,能夠改爲在每個page裏面用before/after畫,每一個page仍是要佔50%,這樣就沒問題。
另一個要考慮的問題是,使用了transform: scale + translateZ可能會致使模糊,一個直接的例子能夠見這個codepen,就是由於用了translateZ或者will-change: transform等觸發了GPU渲染致使模糊了,這個過程多是瀏覽器把當前圖層截一張圖給GPU計算,GPU把這張靜態圖縮放就會模糊。而當咱們把translateZ等有promotion提高做用的屬性去掉以後,在縮放的過程會模糊,可是最終狀態是清晰的。以下圖所示:
在上面的例子裏面咱們用了transform: scale(-1, 1)作水平翻轉,而後還用了translateZ(1px)作上下圖層關係。理論上咱們使用scale可是並無放大或者縮小不該該模糊纔對,可是在windows上的Chrome能夠明顯看到模糊了(在mac上的Chrome是不會模糊的),把translateZ去掉就不會模糊了。因此我想到的解法方法是一開始圖層不要translateZ(使用z-index),只有開始作動畫了才加上translateZ(並去掉z-index),動畫結束後再把translateZ去掉。
當把上面的問題都解決了以後,能夠把它變成一個插件,用的人只要引入,而後初始化一下就搞定了,不用關心這些類怎麼變之類的問題。
而且,因爲一個paper容器有兩個page是正反面的關係,一旦中間忽然插入了一頁就會致使page的正反面關係發生變化,因此這個結構不是很靈活,最好是動態生成,也就是說使用插件的人,把全部的page並列排就行了,而後在插件裏面再從新組織下DOM結構,把在正反面的兩個page放到一個paper裏面。
接着討論下第二種翻書效果的實現。
這個有一個現成的插件turn.js,使用起來很是簡單,咱們簡單討論一下它的內部實現。
這個東西乍看一下,彷佛有曲面的效果:
但其實是沒有的,這個曲線效果是它添加的陰影和漸變產生的視覺效果,當咱們把background-image的漸變去掉以後對比一下就能看出來了:
沒有漸變的假裝以後一會兒就平了。它就變成了一個摺紙的模型——給定一張紙和一個折過去的點,計算一下折過去的旋轉角度和位移。它的源代碼是在fold函數裏面計算的:
它裏面有各類餘弦正弦的計算和角度的判斷,具體實現仍是比較複雜的,沒有深刻去研究,代碼可見turn.js.
還有一個問題是它是怎麼實現三角形裁剪展現的效果?它是在上層又蓋了一個div:
本文討論了兩種翻書效果的實現,重點討論了一下比較簡單的總體翻頁的方式,這種方式主要是作rotateY動畫,同時打開perspective讓其具備景深效果,而且用preserve-3d結合translateZ製造上下層級關係,這種方式可能會存在閃動和模糊的問題,爲了讓翻過去不會閃動關鍵的地方是保證每個page佔寬50%,模糊的問題是由於用了scale加上GPU提高致使的,因此只能經過不寫3d屬性保證清晰。
第二種的效果模型相對比較複雜,簡單分析了一下它的原理和實現方式。主要是計算摺紙過來的角度和位移,上層再蓋一個div隱藏不露出來的部分。而後再加上陰影和漸變製造一種曲面的效果。這種翻書的效果仍是挺好玩的。