建構高效展開和素材資源;收合動畫

Paul Lewis
Stephen McGruer
Stephen McGruer

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;
}

這種做法立即會遇到的問題,是需要為 widthheight 製作動畫。這些屬性需要計算版面配置,並在動畫的每個影格上繪製結果,這可能會耗用大量資源,而且通常會導致您無法達到 60fps。如果您不熟悉這項功能,請參閱轉換成效指南,進一步瞭解轉換程序的運作方式。

錯誤做法:使用 CSS 的 clip 或 clip-path 屬性

除了為 widthheight 製作動畫,您也可以使用 (現已淘汰) 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);
}

雖然比起動畫處理選單元素的 widthheight 更佳,但此方法的缺點是仍會觸發繪製。此外,如果您選擇這條路,clip 屬性會要求所操作的元素為絕對或固定位置,這可能需要額外的處理。

良好:動畫比例

由於這項效果涉及某物變大或縮小,因此您可以使用縮放轉換。這項功能實在太棒了,因為變更轉換不需要版面配置或著色,且瀏覽器可以將其交給 GPU,這表示效果會加速,且更有可能達到 60fps。

這種方法的缺點是需要進行一些設定,這與算繪效能方面的大多數情況相同。不過,這一切都是值得的!

步驟 1:計算開始和結束狀態

使用使用縮放動畫的方法時,第一步是讀取元素,以便瞭解在摺疊和展開時,選單需要的大小。在某些情況下,您可能無法一次取得這兩種資訊,而需要切換某些類別,才能讀取元件的各種狀態。不過,如果您需要這樣做,請務必小心:如果樣式自上次執行後有所變更,getBoundingClientRect() (或 offsetWidthoffsetHeight) 會強制瀏覽器執行樣式和版面配置傳遞作業。

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 倍,以免內容遭到壓縮。請注意以下兩點:

  1. 反向轉換也是縮放作業。這點很棒,因為它也能加速,就像容器上的動畫一樣。您可能需要確保動畫元素取得自己的合成器圖層 (啟用 GPU 來提供協助),為此,您可以將 will-change: transform 新增至元素,如果需要支援較舊的瀏覽器,則可以新增 backface-visiblity: hidden

  2. 必須為每個影格計算反向轉換。這時情況可能會變得較為複雜,因為假設動畫位於 CSS 中且使用了漸變函式,則在製作計數器轉換動畫時,需要對漸變效果本身進行計數。不過,計算 cubic-bezier(0, 0, 0.3, 1) 的反向曲線並非那麼明顯。

因此,您可能會考慮使用 JavaScript 製作動畫效果。畢竟,您可以使用漸變方程式來計算每個影格縮放和反向縮放的值。以 JavaScript 為基礎的動畫的缺點是,當主執行緒 (JavaScript 執行所在位置) 忙於執行其他工作時,就會發生問題。簡單來說,動畫可能會斷斷續續或完全停止,這對使用者體驗不利。

步驟 2:即時建構 CSS 動畫

解決方案 (一開始可能看起來很奇怪) 是使用我們自己的漸變函式,以動態方式建立關鍵影格動畫,然後將其插入頁面,供選單使用。(感謝 Chrome 工程師 Robert Flack 指出這一點!)這項做法的最大優點是,可在合成器上執行會變異轉換的關鍵影格動畫,也就是說,它不會受到主執行緒上的工作影響。

為了製作關鍵影格動畫,我們會從 0 到 100 進行步驟,並計算元素及其內容所需的縮放值。這些內容可簡化為字串,並以樣式元素的形式插入頁面。插入樣式會導致網頁上的「Recalculate Styles」傳遞,這是瀏覽器必須執行的額外工作,但在元件啟動時,瀏覽器只會執行一次。

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;

更進階的版本:圓形揭示效果

您也可以使用這項技巧製作圓形展開和收合動畫。

原則與先前版本大致相同,您可以縮放元素,並反向縮放其直接子項。在這種情況下,縮放的元素具有 50% 的 border-radius,因此會形成圓形,並且會包裝在具有 overflow: hidden另一個元素中,也就是說您不會看到圓形擴展到元素邊界之外。

針對這個特定變化版本,我們要提醒您:由於文字的縮放和反向縮放會導致捨入誤差,因此在低 DPI 螢幕上,Chrome 在動畫期間顯示的文字會模糊。如果你想瞭解詳細資訊,可以將這項錯誤提交項目設為收藏並追蹤

您可以在 GitHub 存放區中找到圓形展開效果的程式碼。

結論

這就是使用縮放轉換來執行效能良好的短片動畫的方法。在理想情況下,我們希望看到剪輯片段動畫加速 (Jake Archibald 製作了 Chromium 錯誤),但在我們達成這個目標之前,請務必謹慎使用 clipclip-path 製作動畫,並避免使用 widthheight 製作動畫。

對於這類效果,使用網頁動畫也很方便,因為它們具有 JavaScript API,但如果您只為 transformopacity 製作動畫,也可以在轉譯器執行緒上執行。很抱歉,網頁動畫的支援功能並不完善,但您可以使用漸進式增強功能 (如有) 來使用這些動畫。

if ('animate' in HTMLElement.prototype) {
    // Animate with Web Animations.
} else {
    // Fall back to generated CSS Animations or JS.
}

在這個情況改變之前,雖然您可以使用以 JavaScript 為基礎的程式庫執行動畫,但您可能會發現,如果將 CSS 動畫轉換為圖片,然後改用圖片,效能會更可靠。同樣地,如果您的應用程式已使用 JavaScript 製作動畫,至少應與現有的程式碼集保持一致,以便提供更優質的服務。

如要查看這項效果的程式碼,請前往 UI Element 範例 GitHub 存放區,並在下方留言告訴我們您如何處理。