NRK 如何運用捲動驅動動畫,讓故事更生動

發布日期:2026 年 2 月 26 日

捲動驅動動畫已從笨拙的主執行緒 JavaScript 實作,轉變為使用新式 CSS 和 UI 功能 (例如捲動時間軸和檢視時間軸) 的流暢、易用的非主執行緒體驗。這項轉變可讓團隊快速製作原型和高效能動畫,同時製作出精緻的捲動式敘事頁面,如本文所示。

NRK 和說故事

NRK (挪威廣播公司) 是挪威的公共廣播服務廣播公司。本文所述實作項目的團隊在挪威文中稱為 Visuelle Historier,大致對應的英文為 Visual Stories。這個團隊會為電視、廣播和網路的編輯專案提供設計、圖像和開發服務,開發視覺識別、內容圖像、特寫文章和新的視覺敘事格式。這個團隊也與 NRK 的設計師和子品牌合作,製作工具和範本,讓你更輕鬆地發布符合 NRK 品牌形象的內容。

NRK 如何使用捲動驅動動畫

透過捲動驅動和捲動觸發的動畫,讓內容更具互動性、更吸引人,並讓讀者更容易記住內容。這種做法特別適合用於非虛構敘事,因為這類內容通常只有少數或完全沒有圖片。

這些動畫有助於強化或創造戲劇性重點、推進故事情節,並發展出與文字相符或加強文字的短小敘事。這些動畫是透過捲動操作驅動,讓使用者可透過捲動操作控制敘述內容的進度。

提升使用者體驗

NRK 的使用者洞察資料顯示,讀者很喜歡這些動畫引導他們專注於內容。在使用者捲動畫面時,系統會標出文字或動畫,方便他們找出重點,並瞭解故事中最關鍵的部分,尤其是在瀏覽時。

此外,動畫圖形可簡化複雜資訊,讓使用者更容易理解關係和隨時間變化的情形。NRK 可透過動態建立、新增或強調資訊,以更具教育意義且更吸引人的方式呈現內容。

設定情境

動畫是設定或強化故事情緒的強大工具。調整動畫的時間、速度和風格,NRK 可喚起與敘事基調相符的情緒。

分割文字並提供視覺緩衝

NRK 經常使用小型動畫插圖,以簡單的插圖或小插圖的形式分隔長篇文字,讓讀者暫時停下來觀看敘述。許多使用者都很喜歡這種變化,認為這有助於分割文字,讓內容更易於消化。他們認為這能讓敘述內容有個適當的停頓。

尊重無障礙需求和使用者偏好

NRK 的公開網頁必須可供所有挪威公民存取。因此,網頁必須尊重使用者減少動畫的偏好設定。所有網頁內容都必須向已啟用這項瀏覽器設定的使用者開放。

設計捲動驅動動畫

NRK 開發並直接將新的捲動動畫工具整合至 Sanity 內容管理系統 (CMS),藉此簡化設計工作流程。這項工具是由開發及維護網站和 CMS 解決方案的團隊共同開發,可讓設計人員輕鬆製作原型,並透過視覺提示實作捲動動畫,以便針對動畫元素的開始和結束位置,以及即時預覽動畫。這項創新功能可讓設計師進一步控管,並直接在 CMS 中加速設計流程。

顯示在工具中捲動至檢視區域的區域。
類似的示例:動畫元素的開始和結束位置的視覺提示,並非實際的 CMS 工具。

瀏覽器中的捲動驅動動畫

以故事為主軸的動畫

The man who wasn't missed

這篇文章報導一名男子在公寓內死亡九年,由於缺乏其他視覺元素,因此必須大量使用插圖。插圖會透過捲動進行動畫處理,以強調敘事內容。例如在夜晚降臨的動畫中,多層建築的燈光會逐漸亮起,直到只剩一間公寓未亮燈為止。這部動畫是使用 NRK 的內部捲動動畫工具建構而成。

文字淡出動畫

永久凍土

本文開頭會簡單介紹內容,就像電影的開場片段。簡潔的文字搭配全螢幕圖像,旨在暗示文章內容,營造期待感,鼓勵讀者深入瞭解整篇文章。標題頁的設計類似電影海報,透過捲動驅動的動畫效果,讓文字以流暢的動畫效果向上和向外移動,強化這種感覺。

.article-section {
  animation: fade-up linear;
  animation-timeline: view();
  animation-range: entry 100% exit 100%;
}

捲動動畫排版

文章標題中的動態字體排版 -「Sick leave」

在「Sjukt sjuke」(大致翻譯為「Sickly sick」) 的介紹中,NRK 希望吸引讀者閱讀一篇關於挪威病假率上升的文章。標題的用意是吸引讀者目光,讓他們知道這不是他們預期的平淡無趣、以數字為主的內容。NRK 團隊希望文字和插圖能配合作品的主題,並透過字體排版和捲動式動畫加以強化。這篇文章使用 NRK News 的新字型和設計設定檔。

<h1 aria-label="sjuke">
  <span>s</span><span>j</span><span>u</span><span>k</span><span>e</span>
<h1>
h1 span {
  display: inline-block;
}
if (window.matchMedia('print, (prefers-reduced-motion: reduce)').matches) {
  return;
}

const heading = document.querySelector("h1");
const letters = heading.querySelectorAll("span");

const timeline = new ViewTimeline({ subject: heading });
const scales = [/**/];
const rotations = [/**/];

for ([index, el] of letters.entries()) {
  el.animate(
    {
      scale: ["1", scales[index]],
      rotate: ["0deg", rotations[index]]
    },
    {
      timeline,
      fill: "both",
      rangeStart: "contain 30%",
      rangeEnd: "contain 70%",
      easing: "ease-out"
    }
  );
}

醒目顯示捲動對齊的項目

機構收容的兒童

讀完文章後,通常會想進一步瞭解相關議題。在關於機構內青少年濫用藥物的文章中,NRK 希望推薦一篇文章供讀者閱讀,並提供其他幾篇文章供讀者選擇。解決方案是使用捲動貼齊和捲動驅動動畫實作的滑動式導覽功能。動畫可確保聚焦於有效元素,並將其他元素調暗。

for (let item of items) {
  const timeline = new ViewTimeline({ subject: item, axis: "inline" });
  const animation = new Animation(effect, timeline);
  item.animate(
    {
      opacity: [0.3, 1, 0.3]
    },
    { timeline, easing: "ease-in-out", fill: "both" }
  );
  animation.rangeStart = "cover calc(50% - 100px)";
  animation.rangeEnd = "cover calc(50% + 100px)";
}

捲動動畫觸發一般動畫

預算

在這篇關於挪威國家預算的文章中,NRK 希望讓原本沉重且枯燥的數字故事,變得更易於閱讀且更貼近讀者。目的是將龐大且難以理解的預算數字細分,讓讀者瞭解自己的稅金用於何處。每個子部分都聚焦於國家預算中的特定項目。讀者的總稅收貢獻以藍色長條圖表示,並分成多個部分,揭露讀者對這些個別項目的貢獻。這項轉場效果是透過捲動驅動動畫達成,該動畫會觸發個別項目的動畫效果。

const timeline = new ViewTimeline({
  subject: containerElement
});

// Setup scroll-driven animation
const scrollAnimation = containerElement.animate(
  {
    "--cover-color": ["blue", "lightblue"],
    scale: ["1 0.2", "1 3"]
  },
  {
    timeline,
    easing: "cubic-bezier(1, 0, 0, 0)",
    rangeStart: "cover 0%",
    rangeEnd: "cover 50%"
  }
);

// Wait for scroll-driven animation to complete
await scrollAnimation.finished;
scrollAnimation.cancel();

// Trigger time-driven animations
for (let [index, postElement] of postElements.entries()) {
  const animation = postElement?.animate(
    { scale: ["1 3", "1 1"] },
    {
      duration: 200,
      delay: index * 33,
      easing: "ease-out",
      fill: "backwards"
    }
  );
}

「我們很早就開始使用捲動驅動動畫,在 Web Animations API 推出之前,我們必須使用捲動事件,後來再搭配 Intersection Observer API。這項作業通常耗時甚久,但現在只要使用 Web Animations 和 Scroll-Driven Animations API,就能輕鬆完成。」Helge Silset,NRK 的前端開發人員

NRK 提供許多不同的網頁元件,可插入其中一個名為 ScrollAnimationDriver (<scroll-animation-driver>) 的自訂元素,支援下列動畫:

  • 使用 [KeyframeEffects](https://developer.mozilla.org/docs/Web/API/KeyframeEffect) 的圖層
  • Lottie 動畫
  • mp4
  • three.js
  • <canvas>

以下範例使用含有 KeyframeEffects 的圖層:

<scroll-animation-driver data-range-start='entry-crossing 50%' data-range-end='exit-crossing 50%'>
  <layered-animation-effect>
    <picture>
      <source />
      <img />
    </picture>

    <picture>
      <source />
      <img />
    </picture>

    <picture>
      <source />
      <img />
    </picture>
  </layered-animation-effect>
</scroll-animation-driver>

NRK 的 <scroll-animation-driver> 自訂元素 JavaScript 實作方式:

export default class ScrollAnimationDriver extends HTMLElement {
  #timeline

  connectedCallback() {
    this.#timeline = new ViewTimeline({subject: this})
    for (const child of this.children) {
      for (const effect of child.effects ?? []) {
        this.#setupAnimationEffect(effect)
      }
    }
  }

  #setupAnimationEffect(effect) {
    const animation = new Animation(effect, this.#timeline) 
    animation.rangeStart = this.rangeStart
    animation.rangeEnd = this.rangeEnd

    if (this.prefersReducedMotion) {
      animation.currentTime = CSS.percent(this.defaultProgress * 100)
    } else {
      animation.play()
    }
  }
}

export default class LayeredAnimationEffect extends HTMLElement {
  get effects() {
    return this.layers.flatMap(layer => toKeyframeEffects(layer))
  }
}

捲動效能

在使用捲動驅動動畫之前,NRK 就已實作效能極佳的 JavaScript,但現在捲動驅動動畫可讓效能更上一層樓,無須擔心捲動時的卡頓情形,即使是在低耗電裝置上也一樣。

  • 非 SDA 任務的時間長度:1 毫秒。
  • SDA 工作時間:0.16 毫秒。
Chrome 開發人員工具的「效能」分頁。
在 Chrome 開發人員工具的「效能」分頁中,以 6 倍 CPU 減速率錄製的內容顯示,在新的影格中,每項工作耗時 0.16 毫秒。

如要進一步瞭解 JavaScript 實作和捲動驅動動畫之間的捲動效能差異,請參閱「捲動驅動動畫效能之個案研究」一文。

無障礙和使用者體驗考量

在許多情況下,NRK 的公開網頁必須讓所有挪威公民都能存取,因此無障礙設計在這些網頁中扮演重要角色。NRK 會確保捲動動畫可透過以下幾種方式存取:

  • 尊重使用者減少動畫的偏好設定:使用媒體查詢 screen and (prefers-reduced-motion: no-preference) 將動畫套用為漸進式增強功能。同時處理列印樣式也很有幫助。
  • 考量裝置種類繁多,且捲動輸入精確度各異:部分使用者可能會以步驟 (空白鍵或上/下鍵、使用螢幕閱讀器前往地標) 捲動,而不會看到整個動畫。確保不會遺漏重要資訊。
  • 謹慎使用顯示或隱藏內容的動畫:如果使用者依賴作業系統 (OS) 縮放功能,可能很難注意到隱藏的內容會在捲動時顯示。避免讓使用者搜尋。如果需要隱藏或顯示內容,請確保內容的顯示和消失位置一致。
  • 避免動畫中的亮度或對比度有大幅變化:由於捲動驅動的動畫取決於使用者控制,突然的亮度變化可能會出現閃爍效果,進而引發部分使用者的癲癎症狀。
@media (prefers-reduced-motion: no-preference) {
  .article-image {
    opacity: 0;
    transition: opacity 1s ease-in-out;
  }
  .article-image.visible {
    opacity: 1;
  }
}

瀏覽器支援

為了讓更多瀏覽器支援 ScrollTimelineViewTimeline,NRK 使用了開放原始碼 polyfill,並有活躍社群提供貢獻

目前,當 ScrollTimeline 無法使用,且使用沒有 CSS 支援的簡化版 polyfill 時,會以條件式方式載入 polyfill。

if (!('ScrollTimeline' in window)) {
  await import('scroll-timeline.js')
}

瀏覽器支援在 CSS 中偵測及處理:

@supports not (animation-timeline: view()) {
  .article-section {
    translate: 0 calc(-15vh * var(--fallback-progress));
    opacity: var(--fallback-progress);
  }
}

@supports (animation-timeline: view()) {
  .article-section {
    animation: --fade-up linear;
    animation-timeline: view();
    animation-range: entry 100% exit 100%;
  }
}

在前述針對不支援的瀏覽器的範例中,NRK 使用 CSS 變數 --fallback-progress 做為備用方案,用於控制 translateopacity 屬性的動畫時間表。

接著,使用 scroll 事件事件監聽器和 JavaScript 中的 requestAnimationFrame 更新 --fallback-progress CSS 變數,如下所示:

function updateProgress() {
  const end = el.offsetTop + el.offsetHeight;
  const start = end - window.innerHeight;
  const scrollTop = document.scrollingElement.scrollTop;
  const progress = (scrollTop - start) / (end - start);
  document.body.style.setProperty('--fallback-progress', clamp(progress, 0, 1));
}


if (!CSS.supports("animation-timeline: view()")) {
  document.addEventListener('scroll', () => {
    if (!visible || updating) {
      return;
    }

    window.requestAnimationFrame(() => {
      updateProgress();
      updating = false;
    });

    updating = true;
  });
}

資源

特別感謝 Google 的 Hannah Van Opstal、Bramus 和 Andrew Kean Guan,以及 NRK 的 Ingrid Reime,感謝他們對這項工作做出寶貴的貢獻。