記錄--怎麼實現一個3d翻書效果

来源:https://www.cnblogs.com/smileZAZ/archive/2023/08/28/17663120.html
-Advertisement-
Play Games

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 本篇主要討論以下兩種翻書動畫的實現: 第一種是整頁翻轉的效果: 這種整頁翻轉的效果主要是做rotateY的動畫,並結合一些CSS的3d屬性實現。 第二種折線翻轉的效果,如下圖所示: 主要是通過計算頁面翻折過來的位置。 這兩種原理上都不是很 ...


這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

本篇主要討論以下兩種翻書動畫的實現:

第一種是整頁翻轉的效果:

這種整頁翻轉的效果主要是做rotateY的動畫,並結合一些CSS的3d屬性實現。

第二種折線翻轉的效果,如下圖所示:

主要是通過計算頁面翻折過來的位置。

這兩種原理上都不是很複雜,需要各個細節配合好,形成一個連貫的翻書動畫。

我們先重點說一下第一種翻頁效果的實現。

1. 基本佈局

這種的實現相對比較簡單,我們先把DOM結構準備好,如下代碼所示:

<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水平翻轉一下:

.page-1-back,
.page-2-back {
    transform: scale(-1, 1);
}
並且.page-1的z-index要比在後面的.page-1-back要高:
.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;
}

需要設置變換中心為左邊中間的位置,效果如下:

 我們發現有幾個問題,第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可以理解為攝像機的位置,如果值越大攝像機就推得越遠。不同值對比如下:

這樣翻書的動畫基本就完成了,從左向右翻也是同樣道理。接下來的問題是怎麼形成連續翻書的動畫。

3. 連續翻書

可以給動畫加一個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那麼就縮短動畫的時間,讓它翻得快一點。相似的處理我已經在《實現內部組件輪播切換效果》討論過,這裡不再重覆。

4. 適配的問題

你可能會擔心動畫結束後修改了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提升作用的屬性去掉之後,在縮放的過程會模糊,但是最終狀態是清晰的。如下圖所示:

5. 變成一個插件

當把上面的問題都解決了之後,可以把它變成一個插件,用的人只要引入,然後初始化一下就搞定了,不用關心這些類怎麼變之類的問題。

並且,由於一個paper容器有兩個page是正反面的關係,一旦中間突然插入了一頁就會導致page的正反面關係發生變化,所以這個結構不是很靈活,最好是動態生成,也就是說使用插件的人,把所有的page併列排就好了,然後在插件裡面再重新組織下DOM結構,把在正反面的兩個page放到一個paper裡面。

 

接著討論下第二種翻書效果的實現。

6. 折線翻書效果的實現

這個有一個現成的插件turn.js,使用起來非常簡單,我們簡單討論一下它的內部實現。

這個東西乍看一下,似乎有曲面的效果:

 但實際上是沒有的,這個曲線效果是它添加的陰影和漸變產生的視覺效果,當我們把background-image的漸變去掉之後對比一下就能看出來了:

 沒有漸變的偽裝之後一下子就平了。它就變成了一個摺紙的模型——給定一張紙和一個折過去的點,計算一下折過去的旋轉角度和位移。它的源代碼是在fold函數裡面計算的:

它裡面有各種餘弦正弦的計算和角度的判斷,具體實現還是比較複雜的,沒有深入去研究,代碼可見turn.js.

還有一個問題是它是怎麼實現三角形裁剪展示的效果?它是在上層又蓋了一個div:

7. 小結

本文討論了兩種翻書效果的實現,重點討論了一下比較簡單的整體翻頁的方式,這種方式主要是做rotateY動畫,同時打開perspective讓其具有景深效果,並且用preserve-3d結合translateZ製造上下層級關係,這種方式可能會存在閃動和模糊的問題,為了讓翻過去不會閃動關鍵的地方是保證每一個page占寬50%,模糊的問題是因為用了scale加上GPU提升導致的,所以只能通過不寫3d屬性保證清晰。

第二種的效果模型相對比較複雜,簡單分析了一下它的原理和實現方式。主要是計算摺紙過來的角度和位移,上層再蓋一個div隱藏不露出來的部分。然後再加上陰影和漸變製造一種曲面的效果。這種翻書的效果還是挺好玩的。

本文轉載於:

https://juejin.cn/post/6844903665216520206

如果對您有所幫助,歡迎您點個關註,我會定時更新技術文檔,大家一起討論學習,一起進步。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ![](https://img2023.cnblogs.com/blog/3076680/202308/3076680-20230825164340432-1938857156.png) # 1. 連接 ## 1.1. 笛卡兒積 ### 1.1.1. 交叉連接(cross join) ### 1.1 ...
  • ![file](https://img2023.cnblogs.com/other/2685289/202308/2685289-20230828190238753-1379880889.png) 視頻貢獻者 | 王維饒 視頻製作者 | 聶同學 編輯整理 | Debra Chen > Apache ...
  • ![file](https://img2023.cnblogs.com/other/3195851/202308/3195851-20230828185450336-2138333694.png) > 時隔兩個月, Apache SeaTunnel 終於迎來大版本更新。此次發佈的 2.3.3 版本在 ...
  • 第14屆中國資料庫技術大會的GaussDB“五高兩易”核心技術,給世界一個更優選擇專場,華為GaussDB首席安全架構師郭亮詳細解讀了GaussDB的高安全之密態等值技術。 ...
  • 8月17日,在第14屆中國資料庫技術大會(DTCC2023)上,天翼雲科技有限公司資料庫首席技術官李躍森以《天翼雲TeleDB持續創新之路》為題發表演講,介紹了天翼雲TeleDB資料庫的發展歷程、技術突破以及國產化能力。 ...
  • * 0.結論先行 * 1.背景介紹 * 2.測試過程 * 3.結果對比 - 附錄 > myloader還預設禁用binlog了 ## 0. 結論先行 重要結論先說:導入大批量數據時,採用GreatSQL 8.0.32-24中新增並行load data特性是最快的,關於該特性的描述詳見:[Change ...
  • 前端下載文件一般使用的是blob 核心的步驟是獲取後端響應的文件流,用blob創建一個臨時的URL,然後創建一個隱藏的<a>標簽,實現下載需求。 那就先上代碼 function download(item) { axios.get(getServerUrl() + "/teacher/output/ ...
  • > 導語:最近開發了一個基於 uniapp 框架的項目,有一些感觸和體會,所以想記錄以下一些技術和經驗,在這裡做一個系列總結,算是對自己做一個交代吧。 ## 目錄 * 簡介 * 全局文件 * 全局組件 * 常用 API * 條件編譯 * 插件開發 ## 簡介 uniapp 是 DCloud 公司於 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...