TL;DR
為短片加入動畫效果時,使用比例轉換功能。您可以透過反向縮放,避免子項在動畫期間被拉長和扭曲。
我們先前已發布最新消息,說明如何建立高效能的視差效果和無限捲動器。在本文中,我們會探討想要製作高成效的短片動畫,牽涉到哪些因素。如要查看示範,請參閱 UI 元素範例 GitHub 存放區。
以展開選單為例:
其中某些建構選項的效能較高。
錯誤:為容器元素設定寬度和高度動畫
您可以使用 CSS 為容器元素的寬度和高度製作動畫。
.menu {
overflow: hidden;
width: 350px;
height: 600px;
transition: width 600ms ease-out, height 600ms ease-out;
}
.menu--collapsed {
width: 200px;
height: 60px;
}
這種做法立即會遇到的問題,是需要為 width
和 height
製作動畫。這些屬性需要計算版面配置,並在動畫的每個影格上繪製結果,這可能會非常耗用資源,而且通常會導致您無法達到 60fps。如果您不熟悉這項功能,請參閱「轉換成效」指南,進一步瞭解轉換程序的運作方式。
錯誤做法:使用 CSS 的 clip 或 clip-path 屬性
除了為 width
和 height
製作動畫,您也可以使用 (現已淘汰) clip
屬性為展開和收合效果製作動畫。或者,您也可以改用 clip-path
。不過,使用 clip-path
的支援度不如 clip
高。但 clip
已淘汰。沒錯。但也不要絕望,畢竟這並非您希望的解決方案!
.menu {
position: absolute;
clip: rect(0px 112px 175px 0px);
transition: clip 600ms ease-out;
}
.menu--collapsed {
clip: rect(0px 70px 34px 0px);
}
雖然比起動畫處理選單元素的 width
和 height
更佳,但這種做法的缺點是仍會觸發繪製。此外,如果您選擇這條路,clip
屬性會要求所操作的元素為絕對或固定位置,這可能需要額外的處理。
佳:為比例動畫加上動畫效果
由於這個效果牽涉到變大和較小,因此您可以使用比例轉換。這項功能實在太棒了,因為變更轉換不需要版面配置或著色,且瀏覽器可以將其交給 GPU,這表示效果會加速,且更有可能達到 60fps。
這種方法的缺點是,如同算繪效能方面的大多數事物,需要進行一些設定。但絕對值得!
步驟 1:計算開始和結束狀態
使用使用縮放動畫的方法時,第一步是讀取元素,瞭解在摺疊和展開時,選單需要的大小。在某些情況下,您無法一一取得上述資訊,這時就需要切換某些類別,才能讀取元件的各種狀態。不過,如果您需要這樣做,請務必小心:如果樣式自上次執行後有所變更,getBoundingClientRect()
(或 offsetWidth
和 offsetHeight
) 會強制瀏覽器執行樣式和版面配置傳遞作業。
function calculateCollapsedScale () {
// The menu title can act as the marker for the collapsed state.
const collapsed = menuTitle.getBoundingClientRect();
// Whereas the menu as a whole (title plus items) can act as
// a proxy for the expanded state.
const expanded = menu.getBoundingClientRect();
return {
x: collapsed.width / expanded.width,
y: collapsed.height / expanded.height
};
}
以選單為例,我們可以合理假設選單會以自然比例 (1, 1) 開始。這個自然比例代表擴大狀態,也就是說,您需要從縮小的版本 (上方計算的結果) 製作動畫,再回復到自然比例。
等一下!這麼一來,選單的內容也會調整,對吧?是的,請參閱下方說明。
那麼你能做些什麼呢?您可以將 counter- 轉換套用至內容,舉例來說,如果容器縮小到正常大小的 1/5,您可以將內容縮放 5 倍,以免內容遭到壓縮。請注意以下兩點:
反向轉換也是縮放作業。這點很棒,因為它也能加速,就像容器上的動畫一樣。您可能需要確保動畫元素擁有自己的合成器圖層 (啟用 GPU 協助),為此,您可以將
will-change: transform
新增至元素,如果需要支援較舊的瀏覽器,則可以新增backface-visiblity: hidden
。必須為每個影格計算反向轉換。這就有點麻煩了,因為假定動畫位於 CSS 中,並使用了加/減速函式,因此在為計數器轉換產生動畫效果時,必須反制加/減速。不過,計算
cubic-bezier(0, 0, 0.3, 1)
的反向曲線並非那麼明顯。
因此,您可能會考慮使用 JavaScript 製作動畫效果。接著,您可以使用加/減速計算每個影格的比例和反尺度值。以 JavaScript 為基礎的動畫的缺點是,當主執行緒 (JavaScript 執行所在位置) 忙於執行其他工作時,就會發生問題。簡單來說,動畫可能會斷斷續續或完全停止,這對使用者體驗不利。
步驟 2:即時建構 CSS 動畫
解決方案 (一開始可能看起來很奇怪) 是使用我們自己的漸變函式,以動態方式建立關鍵影格動畫,然後將其插入頁面,供選單使用。(感謝 Chrome 工程師 Robert Flack 指出這一點!)這種做法的主要優點是,會變更轉換的主要畫面格動畫可以在合成器上執行,表示該動畫不受主執行緒上的工作影響。
為了製作關鍵影格動畫,我們會從 0 到 100 進行步驟,並計算元素及其內容所需的縮放值。這些資料可簡化為字串,並以樣式元素的形式插入頁面。插入樣式會導致網頁上執行「重新計算樣式」傳遞作業,這是瀏覽器必須執行的額外工作,但只會在元件啟動時執行一次。
function createKeyframeAnimation () {
// Figure out the size of the element when collapsed.
let {x, y} = calculateCollapsedScale();
let animation = '';
let inverseAnimation = '';
for (let step = 0; step <= 100; step++) {
// Remap the step value to an eased one.
let easedStep = ease(step / 100);
// Calculate the scale of the element.
const xScale = x + (1 - x) * easedStep;
const yScale = y + (1 - y) * easedStep;
animation += `${step}% {
transform: scale(${xScale}, ${yScale});
}`;
// And now the inverse for the contents.
const invXScale = 1 / xScale;
const invYScale = 1 / yScale;
inverseAnimation += `${step}% {
transform: scale(${invXScale}, ${invYScale});
}`;
}
return `
@keyframes menuAnimation {
${animation}
}
@keyframes menuContentsAnimation {
${inverseAnimation}
}`;
}
好奇心切的讀者可能會想知道 for 迴圈內的 ease()
函式。您可以使用像這樣的元件,將 0 到 1 的值對應至易於使用的等值。
function ease (v, pow=4) {
return 1 - Math.pow(1 - v, pow);
}
您也可以使用 Google 搜尋來查看類似的圖表。好方便!如果您需要其他加/減速方程式,請參考 Soledad Penadés 的 Tween.js,其中包含整個堆積的堆積。
步驟 3:啟用 CSS 動畫
這些動畫已在 JavaScript 中建立並烘焙至網頁,因此最後一個步驟是切換啟用動畫的類別。
.menu--expanded {
animation-name: menuAnimation;
animation-duration: 0.2s;
animation-timing-function: linear;
}
.menu__contents--expanded {
animation-name: menuContentsAnimation;
animation-duration: 0.2s;
animation-timing-function: linear;
}
這會導致先前步驟中建立的動畫開始執行。由於已完成烘焙的動畫已放慢,因此需要將時間函式設為 linear
,否則您會在每個關鍵影格之間放慢,看起來會很奇怪!
如要將元素收合,有兩種方法:更新 CSS 動畫,讓動畫反向執行,而非正向執行。這麼做沒問題,但動畫的「感覺」會反轉,因此如果您使用漸弱曲線,反轉的動畫會讓人覺得「慢慢地」,讓人覺得動畫速度緩慢。更適當的解決方案是建立第二組動畫,用於收合元素。您可以使用與展開主要影格動畫完全相同的方式建立這些動畫,但請交換起始和結束值。
const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;
更進階的版本:圓形揭示效果
您也可以使用這項技巧製作圓形展開和收合動畫。
原則與先前版本大致相同,那就是您可以調整元素的資源配置,並對其直接子項進行計數器縮放。在這種情況下,縮放的元素 border-radius
為 50%,因此會形成圓形,並且會被另一個具有 overflow: hidden
的元素包裝,也就是說,您不會看到圓形擴展到元素邊界之外。
針對這個特定變化版本,我們要提醒您:由於文字的縮放和反向縮放會導致捨入誤差,因此在低 DPI 螢幕上,Chrome 在動畫期間顯示的文字會模糊。如果你有興趣瞭解詳情,可以參考我們回報錯誤,之後可以按照指示加上星號。
您可以在 GitHub 存放區中找到圓形展開效果的程式碼。
結論
這就是使用縮放轉換來執行效能良好的短片動畫的方法。在完美情況下,如果看到剪輯動畫加速效果會很好 (這是由 Jake Archibald 產生的Chromium 錯誤),但在此之前,如要為 clip
或 clip-path
建立動畫效果,請務必小心,並絕對避免為 width
或 height
產生動畫效果。
使用網路動畫執行這類效果也很有幫助,因為它們具有 JavaScript API,但如果您只為 transform
和 opacity
動畫,則可以在合成器執行緒上執行。很抱歉,網頁動畫的支援功能並不完善,但您可以使用漸進式增強功能 (如有) 來使用這些動畫。
if ('animate' in HTMLElement.prototype) {
// Animate with Web Animations.
} else {
// Fall back to generated CSS Animations or JS.
}
在這個情況改變之前,雖然您可以使用以 JavaScript 為基礎的程式庫執行動畫,但您可能會發現,如果將 CSS 動畫轉換為靜態圖片,然後改用這些圖片,效能會更可靠。同樣地,如果您的應用程式已使用 JavaScript 製作動畫,至少應與現有的程式碼集保持一致,以便提供更優質的服務。
如要瞭解這個效果的程式碼,請參閱 UI 元素範例 GitHub 存放區。一如往常,歡迎透過下方的留言告訴我們您怎麼做。