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

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 進行步驟,並計算元素及其內容所需的縮放值。這些資料可簡化為字串,並以樣式元素的形式插入頁面。插入樣式會導致網頁上執行「重新計算樣式」傳遞作業,這是瀏覽器必須執行的額外工作,但只會在元件啟動時執行一次。

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 錯誤),但在此之前,如要為 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 元素範例 GitHub 存放區。一如往常,歡迎透過下方的留言告訴我們您怎麼做。