建構輪轉介面投影片效果

輪轉投影片效果可為輪轉介面增添視覺趣味。使用者捲動投影片時,每張投影片在進入、置中和離開捲動區時,都能播放動畫。例如,投影片可以淡入和淡出、旋轉或縮放大小。打造生動有趣的互動體驗。與簡單的進入/退出動畫不同,這項效果會使用單一連續動畫,控制投影片在整個捲動區塊中的外觀。

導入方式

建立輪轉投影片效果的方法如下:

  1. 建立捲動器:這個元素會做為輪轉介面投影片的容器。在本範例中,它使用 overflow-x: scroll 允許水平捲動。

    <ul class="scroller">
      <li class="entry">1</li>
      <li class="entry">2</li>
      <li class="entry">3</li>
      …
    </ul>
    
    .scroller {
      overflow-x: scroll;
    }
    
  2. 定義動畫:建立 CSS 動畫,定義投影片在捲動區中移動時的不同狀態。您可以為動畫的任何部分定義主要畫面格。舉例來說,您可以加入 50% 關鍵影格,定義投影片位於捲動區塊中央時的狀態。在這個範例中,scale 屬性會使投影片在接近中心時放大,在遠離中心時縮小。

    @keyframes animate {
      0% {
        scale: 0.5;
      }
      50% {
        scale: 1;
      }
      100% {
        scale: 0.5;
      }
    }
    
  3. 套用動畫和 view-timeline:將動畫附加至輪轉介面投影片,並連結至 view-timeline,以便在元素捲動容器時追蹤元素。

    .scroller > * {
      animation: animate auto linear both;
      animation-timeline: view(inline);
    }
    

    根據預設,view() 會追蹤 block 軸上的元素。如需在 inline 軸上追蹤,可以使用 view(inline)。

範例程式碼

這段程式碼會使用匿名檢視畫面時間軸,在捲動時為水平捲動器的輪轉介面項目建立動畫:

@keyframes animate {
  0% {
    scale: 0.5;
  }

  50% {
    scale: 1;
  }

  100% {
    scale: 0.5;
  }
}

.scroller > * {
  /* Applies the animation using an `auto` duration */
  animation: animate auto linear both;
  /* Sets the animation timeline to use an anonymous view progress timeline, tracking the element's progress through the scroller on the inline axis */
  animation-timeline: view(inline);
}

這段程式碼會使用具名檢視畫面時間軸,在捲動時為水平捲動器的輪轉介面項目建立動畫:

@keyframes animate {
  0% {
    scale: 0.5;
  }

  50% {
    scale: 1;
  }

  100% {
    scale: 0.5;
  }
}

/* This creates a named view-timeline on each carousel item. The timeline is used to drive the animation that is applied on the same element. */
.scroller > * {
  /* Applies the animation using an `auto` duration */
  animation: animate auto linear both;
  /* Defines a named view progress timeline, tracking the element's progress through the scroller on the inline axis */
  view-timeline: --item inline;
  /* Sets the animation timeline to use the named view progress timeline defined above */
  animation-timeline: --item;
}

最佳做法

使用捲動驅動動畫時,請務必遵循幾項最佳做法,確保體驗流暢且無障礙:

  • 務必加入功能偵測:並非所有瀏覽器都支援捲動驅動動畫。使用 @supports ((animation-timeline: view()) and (animation-range: entry)) 檢查支援情形,並為不支援的瀏覽器提供備用方案。
    • 這裡必須加入 (animation-range: entry) 檢查,篩除僅部分支援的瀏覽器。
    • 請勿使用 scroll-timeline-polyfill 套件做為備用策略,因為該套件的功能不完整,且有許多已知問題。
    • 如果動畫僅用於裝飾,請選擇漸進增強,且「不要」提供備用方案。
  • 務必尊重使用者偏好:部分使用者偏好網頁上較少動態效果。使用 prefers-reduced-motion 媒體查詢,為這些使用者停用或減少動畫。
  • 建議只為高效能 CSS 屬性製作動畫:如要讓動畫最流暢,請只為瀏覽器合成器執行緒可處理的屬性製作動畫,例如 transform 和 opacity。為 width 或 height 等其他屬性建立動畫,可能會導致效能問題。
  • 建議使用正確的宣告順序:使用 animation 簡寫屬性時,請在之後宣告 animation-timeline 和 animation-range,以免簡寫重設時間軸。

如果多個 DOM 元素需要根據相同時間軸建立動畫,或您需要為已定義 view-timeline 的元素子項建立動畫,建議使用已命名的 view-timeline。如果動畫元素也是定義 view-timeline 的元素,您可以使用 view() 建立匿名檢視時間軸。

使用 view() 函式建立捲動驅動動畫時:

  • 選用:明確指定要追蹤的軸:如果不是指定預設的 block 軸 (例如在水平捲軸中),請明確指定要追蹤的軸,使用 view(block) 或 view(inline)。

使用 view-timeline 屬性建立捲動驅動動畫時:

  • 務必使用 CSS <dashed-ident> 做為名稱 (例如 view-timeline: --my-custom-name)
  • 選用:明確指定要追蹤的軸。如果不是指定預設的 block 軸 (例如在水平捲動器中),請使用 view-timeline-axis 明確指定要追蹤的軸。
  • 務必確保查詢範圍有效:如果宣告 view-timeline 的元素不是動畫元素的平面樹狀結構祖系,請在共用祖系上使用 timeline-scope,提高 view-timeline 名稱的瀏覽權限。

瀏覽器支援和備用策略

因此,通常需要備用策略。

如果瀏覽器不支援捲動驅動動畫,可以使用備用方案重新建立視覺效果。備用方案通常是使用捲動事件監聽器 (用於 ScrollTimeline 效果) 或 IntersectionObserver API (用於 ViewTimeline 效果) 建構。

如果瀏覽器內建支援捲動式動畫,請一律使用原生 CSS 實作,因為這類實作的效能較佳。

請注意,並非所有效果都能使用備用方法重新建立。

針對這個用途,下列指令碼會為不支援捲動驅動動畫的瀏覽器套用備用方案。這個函式會使用 Web Animations API (Element.animate()),為輪轉介面中的每個項目建立暫停的動畫。接著,它會監聽捲動器上的 scroll 事件,並根據項目在捲動器中的捲動進度,更新每個動畫的 currentTime。

// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
  const scroller = document.querySelector('.scroller');
  const entries = document.querySelectorAll('.entry');

  // Create a map to store animations
  const animations = new Map();

  entries.forEach(entry => {
    const animation = entry.animate(
      {
        scale: ['0.5', '1', '0.5']
      },
      {
        duration: 1, // We'll control the time ourselves
        fill: 'both'
      }
    );
    animation.pause();
    animations.set(entry, animation);
  });

  // Update animations on scroll
  const tick = () => {
    const scrollerRect = scroller.getBoundingClientRect();

    entries.forEach(entry => {
      const animation = animations.get(entry);
      if (!animation) return;

      const entryRect = entry.getBoundingClientRect();
      const progress = (entryRect.left + entryRect.width / 2 - scrollerRect.left) / scrollerRect.width;

      animation.currentTime = progress;
    });
  };
    
  scroller.addEventListener('scroll', tick);
  tick();
}