早年我參與一個電子雜誌專案,需求是模擬真實雜誌的翻頁動畫效果。同事推薦了一篇 CodeProject 的經典文章,最終成功達成預期效果。今日嘗試將該文翻譯整理,期盼能幫助英文不佳的開發者;若英文能力許可,建議直接閱讀 英文原文。翻譯水平有限,敬請包涵。
圖 1 翻頁效果
本文介紹如何在電子書或電子相簿並列顯示圖片時,建立逼真的翻頁效果,模擬實體書本翻頁的視覺體驗。
靈感源自對可重用圖片顯示元件的需求,採用 C# 與 GDI+ 實現,旨在深化對 .NET 圖形程式設計的理解。核心演算法參考自 Sham Bhangal 的文章〈Flash MX 中的翻頁效果〉(原文連結),該文闡述如何利用對稱線控制動畫中頁面的可見區域。雖然 Flash MX 與 GDI+ 實作細節不同,但「以對稱線計算可見部分」的核心概念相通。
假設第 3、4 頁為當前頁,使用者即將翻至第 5、6 頁;換言之,我們要呈現第 4 頁被翻動的過程。
圖 2 關鍵區域與參數
整個翻頁動畫可歸納為五個步驟:
原文指出:當前頁(第 3、4 頁)不參與翻頁動畫,意即每一面皆先以無裁剪、無轉換的方式完整繪製,區域 B、C 再疊加於其上。
「熱點」一詞源自參考文獻,代表翻頁時折痕在水平軸上的位置,亦即對稱線的起點。動畫過程中,熱點從頁邊緣移動至書脊中央;從頁邊到熱點的距離 x 隨翻頁而變化。當熱點抵達書脊中央時動畫結束,此時 x = PAGE_WIDTH。
對稱線即翻頁時的折痕,用於計算翻頁過程中各頁面的可見區域。程式中對稱線控制兩項關鍵要素:
對稱線可用下列公式描述:
a = 45 + (45 * x) / PAGE_WIDTH
h = x * Tan(a)
角度 a 隨 x 變化:x=0 時 a=45°,x=PAGE_WIDTH 時 a=90°。動畫伊始 x=0,B、C 不可見;隨 x 增加,對稱線與頁邊、高度 h 構成直角三角形。當 h >= PAGE_HEIGHT 時,交集區域由三角形轉為梯形(高度固定為 PAGE_HEIGHT),圖 1 即為此狀態。無論三角形或梯形,該閉合路徑即為 B、C 兩區域的裁剪邊界。若在原始碼中將 INCLUDE_DRAW_GRAPHICS_PATH 設為 true,程式會在閉合路徑外繪製金色輪廓,便於除錯觀察。
即第 4 頁扣除 B、C 後仍可見的區域。
來源為正在翻動頁面之下的頁面(本例為第 6 頁)。其可見範圍由對稱線與頁面形成的閉合曲線裁剪而成;上層頁面(第 4 頁)對應部分會被直接裁剪移除。隨 x 增加(a 同步增大),此可見區域逐漸擴大。若將原始碼 INCLUDE_UNDERSIDE_PAGE_IN_ANIMATION 設為 false,可移除區域 C,僅觀察 A、B 關係。
代表第 5 頁的可見部分。同樣由對稱線決定,但位於對稱線左側。例如翻第 4 頁時,B 為第 6 頁右下部,C 為第 5 頁左下部。圖 3 展示 B、C 空間關係。
圖 3 B 與 C 之間的關係
繪製區域 C 時,需建立一個輔助的後台圖像 pageUndersideImage,其裁剪區域同樣由對稱線決定。第 5 頁內容繪製至該緩衝區,再貼近區域 B 繪製。
後台圖像備妥後,依下列步驟繪製:
180 - 2a 度(參見圖 4)。(-x, -PAGE_HEIGHT) 繪製含區域 C 的圖像(參見圖 5)。
圖 4 調整角度以匹配對稱線
圖 5 旋轉座標系
實作中發現 B、C 沿對稱線銜接處易出現色彩溢出(如第 4 頁紅色像素外洩)。採用兩種變通方案:
g.PixelOffsetMode = PixelOffsetMode.Half;PathTranslationMatrix.Translate((float)hotSpot.Origin.X + 1, (float)hotSpot.Origin.Y);若啟用第一種方案,請將原始碼 USE_PIXEL_MODE_OFFSET 設為 true。
每一幀皆在 timer1_Tick 中繪製至緩衝區 CurrentShownBitmap,再由 CurrentShownBitmap 直接呈現在螢幕上。
計算每幀 B、C 區域圖像的關鍵方法:
private GraphicsPath GetPageUnderGraphicsPath(int x,
ref double a, int height, int width,
bool isUnderSide, TurnType type)
x:頁邊到熱點的距離。height、width:當前頁顯示尺寸。isUnderSide:是否計算區域 C(翻動頁背面)。type:左翻或右翻。a:傳址參數,回傳當前角度,供繪製區域 C 前旋轉座標使用(對應圖 4、5)。元件內建示範圖片,初始化邏輯位於 LoadSamples(),於 Sample 建構子中呼叫。
透過 Timer 控制翻頁速度,公開屬性 TickSpeed(毫秒)調整間隔;動畫幀數由熱點每次移動距離決定,公開屬性暴露目前移動距離 x。
針對背頁頂部裁剪問題,提供 HeightAdjustment 屬性在控制項頂部保留空白區域。
提供兩個公開方法:animateRightPageTurn() 與 animateLeftPageTurn()。
原作者嘗試解析非線性方程 h = x * Tan(45 + (45 * x) / PAGE_WIDTH) 在 h = PAGE_HEIGHT 時的 x 值,採用暴力求解,程式碼已包含在原始碼中。期待社群提供更優雅的數學解法。