正在打造 Chrometober!

這款 Chrometober 的故事讓人愛不釋手,想分享有趣又令人恐懼的提示和秘訣。

Designcember 後,我們想今年的開發者打造了 Chrometober,以便在社群和 Chrome 團隊推廣並分享網路內容。Designcember 示範了容器查詢的用法,而今年我們要介紹 CSS 捲動連結動畫 API。

如要瞭解捲動式書籍體驗,請前往 web.dev/chrometober-2022

總覽

這項專案的目標是提供獨特的體驗,醒目顯示捲動連結的動畫 API。然而,雖然獨樹一格,但使用者還是能享有回應和便利的體驗。這個專案也很適合用來測試正在進行開發的 API polyfill,除了可以嘗試不同的技術和工具。盡情歡度佳節!

我們的團隊架構如下所示:

草擬捲動式體驗

Chrometober 的靈感是從 2022 年 5 月,第一個團隊異地送回。我們從許多塗鴉中開始思考如何引導使用者沿著某種形式的分鏡腳本捲動內容。受到電玩遊戲的啟發,我們視為透過墓地和鬼屋等場景進行捲動體驗。

筆記本放在桌子上,上面有與專案相關的各種塗鴉和塗鴉。

很榮幸能自由發揮創意,以出乎意料的方向著手建立第一個 Google 專案。這是初期使用者可能瀏覽內容的原型。

當使用者向右捲動時,方塊會旋轉並放大。但我決定卸下這個想法,思考如何才能為各種裝置大小的裝置提供良好的使用者體驗。而是傾向著眼於我過去製作的作品。2020 年,我幸好可以使用 GreenSock's ScrollTrigger 建立版本示範。

我製作的示範影片之一是 3D-CSS 書籍,而且使用者捲動畫面時,會翻面。你覺得 Chrometober 的效果更加出色。捲動連結動畫 API 是該功能的完美替換工具。這同樣可以與 scroll-snap 搭配使用,您再也知道!

這項專案的插畫家 Tyler Reed 能憑藉我們不斷改變的想法,有效調整設計。Tyler 將各種創意構想化為現實,是十分出色的成果。這個過程非常有趣。我們希望達成這個目標的一大重點,就是將各項功能細分為獨立的區塊。我們可以將這些片段組合成不同場景,然後挑選並呈現出栩栩如生的內容。

構圖場景之一,有蛇、一個把手臂冒出的棺材、一個有魔杖的狐狸、一棵充滿恐怖臉的樹,以及拿著南瓜燈籠的花環。

主要想法在於,使用者將一整本書後,就能存取這些內容區塊。還能與逗趣的破折號互動,包括體驗內建的彩蛋;例如在一個鬼屋中畫出眼睛跟你的遊標的肖像畫,或是媒體查詢所觸發的細微動畫。這些想法和功能會在使用者捲動畫面時呈現動畫效果。一開始的想法就是一個殭屍兔子,在使用者捲動時沿著 X 軸興起,並隨之平移。

熟悉 API

我們需要一本書,才能開始玩個別功能和彩蛋。因此,我們決定藉此機會測試即將推出的 CSS 捲動連結動畫 API 功能集。目前任何瀏覽器都不支援捲動連結的動畫 API。不過,在開發 API 時,互動團隊的工程師一直致力於執行 polyfill。這樣就能在 API 開發時測試 API 的形狀。也就是說,我們現在可以使用這個 API,而有趣的專案非常適合用來試用實驗功能及提供意見回饋。請參閱本文的內容,瞭解我們學到的內容,以及我們能提供的意見。

整體來說,您可以使用這個 API 連結要捲動的動畫。請注意,您無法在捲動時觸發動畫,這有時可能稍後會再顯示。捲動連結的動畫也可分為兩大類別:

  1. 會回應捲動位置的廣告素材。
  2. 回應元素在捲動容器中的位置。

如要建立後者,我們會使用透過 animation-timeline 屬性套用的 ViewTimeline

以下是在 CSS 中使用 ViewTimeline 的情況範例:

.element-moving-in-viewport {
  view-timeline-name: foo;
  view-timeline-axis: block;
}

.element-scroll-linked {
  animation: rotate both linear;
  animation-timeline: foo;
  animation-delay: enter 0%;
  animation-end-delay: cover 50%;
}

@keyframes rotate {
 to {
   rotate: 360deg;
 }
}

我們使用 view-timeline-name 建立 ViewTimeline 並定義其軸。在這個範例中,block 參照了邏輯 block。動畫會連結到使用 animation-timeline 屬性捲動。animation-delayanimation-end-delay (在本文撰寫時) 是定義階段的方式。

這些階段定義動畫連結的時間點,相對於捲動容器中的元素位置。在本例中,我們說是在元素進入捲動容器時開始播放動畫 (enter 0%)。當它佔了捲動容器的 50% (cover 50%) 時,即可結束。

以下是實際運作的示範:

您也可以將動畫連結至在可視區域中移動的元素。只要將 animation-timeline 設為該元素的 view-timeline 即可。這種做法很適合清單動畫等情境。此行為類似於在使用 IntersectionObserver 的項目上建立動畫效果

element-moving-in-viewport {
  view-timeline-name: foo;
  view-timeline-axis: block;
  animation: scale both linear;
  animation-delay: enter 0%;
  animation-end-delay: cover 50%;
  animation-timeline: foo;
}

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

透過這種方式,Mover 會在進入可視區域時放大,進而觸發「旋轉圖示」的旋轉動作。

實驗結果顯示,這個 API 可以搭配 scroll-snap 順利運作。如果想在書籍中貼齊頁面,將捲動畫面和 ViewTimeline 結合在一起。

模擬機制的原型

經過幾次實驗後,我發現書籍原型可以正常運作。你可以水平捲動來翻頁。

在示範影片中,您可以看到各種以虛線邊框標示的各種觸發條件。

標記大致如下:

<body>
  <div class="book-placeholder">
    <ul class="book" style="--count: 7;">
      <li
        class="page page--cover page--cover-front"
        data-scroll-target="1"
        style="--index: 0;"
      >
        <div class="page__paper">
          <div class="page__side page__side--front"></div>
          <div class="page__side page__side--back"></div>
        </div>
      </li>
      <!-- Markup for other pages here -->
    </ul>
  </div>
  <div>
    <p>intro spacer</p>
  </div>
  <div data-scroll-intro>
    <p>scale trigger</p>
  </div>
  <div data-scroll-trigger="1">
    <p>page trigger</p>
  </div>
  <!-- Markup for other triggers here -->
</body>

捲動畫面時,書頁會隨之轉動,但可收齊或關閉書籍。這取決於觸發條件的捲軸對齊方式。

html {
  scroll-snap-type: x mandatory;
}

body {
  grid-template-columns: repeat(var(--trigger-count), auto);
  overflow-y: hidden;
  overflow-x: scroll;
  display: grid;
}

body > [data-scroll-trigger] {
  height: 100vh;
  width: clamp(10rem, 10vw, 300px);
}

body > [data-scroll-trigger] {
  scroll-snap-align: end;
}

目前未在 CSS 中連結 ViewTimeline,而是在 JavaScript 中使用 Web Animation API。這麼做還有一個額外好處,那就是可循環播放一組元素並產生所需的 ViewTimeline,而不必手動逐一建立。

const triggers = document.querySelectorAll("[data-scroll-trigger]")

const commonProps = {
  delay: { phase: "enter", percent: CSS.percent(0) },
  endDelay: { phase: "enter", percent: CSS.percent(100) },
  fill: "both"
}

const setupPage = (trigger, index) => {
  const target = document.querySelector(
    `[data-scroll-target="${trigger.getAttribute("data-scroll-trigger")}"]`
  );

  const viewTimeline = new ViewTimeline({
    subject: trigger,
    axis: 'inline',
  });

  target.animate(
    [
      {
        transform: `translateZ(${(triggers.length - index) * 2}px)`
      },
      {
        transform: `translateZ(${(triggers.length - index) * 2}px)`,
        offset: 0.75
      },
      {
        transform: `translateZ(${(triggers.length - index) * -1}px)`
      }
    ],
    {
      timeline: viewTimeline,
      …commonProps,
    }
  );
  target.querySelector(".page__paper").animate(
    [
      {
        transform: "rotateY(0deg)"
      },
      {
        transform: "rotateY(-180deg)"
      }
    ],
    {
      timeline: viewTimeline,
      …commonProps,
    }
  );
};

const triggers = document.querySelectorAll('[data-scroll-trigger]')
triggers.forEach(setupPage);

我們會為每個觸發條件產生 ViewTimeline。接著,我們會使用該 ViewTimeline 為觸發條件的相關網頁建立動畫效果。連結網頁的動畫即可捲動。就動畫來說,我們會在 Y 軸上旋轉頁面元素,藉此旋轉頁面。我們也會在 Z 軸上翻譯網頁,因此運作方式就像閱讀書籍一樣。

平台比一比

等到我們擬好這個機制後,就能專心將 Tyler 的插圖呈現生動呈現。

天文

團隊於 2021 年使用 Astro for Designcember,後來想再次透過 Chrometober 使用這款應用程式。開發人員能夠將內容分解成多個元件,而本專案就很適合採用這項專案。

書籍本身是元件,它也包含一組網頁元件。每個頁面都有兩面,並設有背景。頁面端的子項是可輕鬆新增、移除和定位的元件。

製作書籍

對我來說,讓封鎖機制能輕鬆管理非常重要。我也希望能夠協助其他團隊成員輕鬆做出貢獻。

高階網頁是由設定陣列定義。陣列中的每個頁面物件都會定義頁面的內容、背景和其他中繼資料。

const pages = [
  {
    front: {
      marked: true,
      content: PageTwo,
      backdrop: spreadOne,
      darkBackdrop: spreadOneDark
    },
    back: {
      content: PageThree,
      backdrop: spreadTwo,
      darkBackdrop: spreadTwoDark
    },
    aria: `page 1`
  },
  /* Obfuscated page objects */
]

這些項目會傳遞至 Book 元件。

<Book pages={pages} />

Book 元件是套用捲動機制及建立書籍頁面的地方。我們採用與原型相同的機制,但會共用在全域中建立的多個 ViewTimeline 例項。

window.CHROMETOBER_TIMELINES.push(viewTimeline);

如此一來,我們就可以分享在其他地方使用的時間軸,而不必重新建立時間軸。稍後會再詳細討論。

網頁組成

每個頁面都是清單內的清單項目:

<ul class="book">
  {
    pages.map((page, index) => {
      const FrontSlot = page.front.content
      const BackSlot = page.back.content
      return (
        <Page
          index={index}
          cover={page.cover}
          aria={page.aria}
          backdrop={
            {
              front: {
                light: page.front.backdrop,
                dark: page.front.darkBackdrop
              },
              back: {
                light: page.back.backdrop,
                dark: page.back.darkBackdrop
              }
            }
          }>
          {page.front.content && <FrontSlot slot="front" />}    
          {page.back.content && <BackSlot slot="back" />}    
        </Page>
      )
    })
  }
</ul>

系統會將定義的設定傳送到各個 Page 執行個體。這類網頁使用 Astro 的版位功能在每個網頁中插入內容。

<li
  class={className}
  data-scroll-target={target}
  style={`--index:${index};`}
  aria-label={aria}
>
  <div class="page__paper">
    <div
      class="page__side page__side--front"
      aria-label={`Right page of ${index}`}
    >
      <picture>
        <source
          srcset={darkFront}
          media="(prefers-color-scheme: dark)"
          height="214"
          width="150"
        >
        <img
          src={lightFront}
          class="page__background page__background--right"
          alt=""
          aria-hidden="true"
          height="214"
          width="150"
        >
      </picture>
      <div class="page__content">
        <slot name="front" />
      </div>
    </div>
    <!-- Markup for back page -->
  </div>
</li>

此程式碼主要用於設定結構。貢獻者在大部分情況下都可以撰寫書籍內容,不必觸碰此代碼。

背景幕

「廣告素材」轉向書籍分割各個部分,比以往更容易多了一本書籍,而且每本書都取自原設計。

書頁插圖,書中有一顆蘋果樹。墓園有多座頭石,在大月前的空中設有一根蝙蝠。

既然我們決定好書籍的顯示比例,每個頁面的背景都可以加上圖片元素。只要將該元素的寬度設為 200%,並根據頁面端使用 object-position,就能解決問題。

.page__background {
  height: 100%;
  width: 200%;
  object-fit: cover;
  object-position: 0 0;
  position: absolute;
  top: 0;
  left: 0;
}

.page__background--right {
  object-position: 100% 0;
}

網頁內容

我們來看看建立一個頁面。第三頁有一隻在樹上彈出的貓頭鷹。

該元件會填入設定中定義的 PageThree 元件。這是 Astro 元件 (PageThree.astro)。這些元件看起來像是 HTML 檔案,但頂端與前端類似的程式碼圍欄。這樣我們就能匯入其他元件,例如匯入其他元件。第 3 頁的元件如下所示:

---
import TreeOwl from '../TreeOwl/TreeOwl.astro'
import { contentBlocks } from '../../assets/content-blocks.json'
import ContentBlock from '../ContentBlock/ContentBlock.astro'
---
<TreeOwl/>
<ContentBlock {...contentBlocks[3]} id="four" />

<style is:global>
  .content-block--four {
    left: 30%;
    bottom: 10%;
  }
</style>

同理,網頁本身就是不可分割的。這些 API 以眾多功能為建構基礎。第 3 頁含有內容區塊和互動式貓頭鷹,因此每個區塊各各有一個元件。

內容區塊是書籍所含內容的連結。這些權限也可由設定物件驅動。

{
 "contentBlocks": [
    {
      "id": "one",
      "title": "New in Chrome",
      "blurb": "Lift your spirits with a round up of all the tools and features in Chrome.",
      "link": "https://www.youtube.com/watch?v=qwdN1fJA_d8&list=PLNYkxOF6rcIDfz8XEA3loxY32tYh7CI3m"
    },
    …otherBlocks
  ]
}

這項設定會匯入需要內容區塊的位置。接著,相關區塊設定會傳遞至 ContentBlock 元件。

<ContentBlock {...contentBlocks[3]} id="four" />

以下舉例說明我們如何使用頁面的元件來定位內容。此處會放置內容區塊。

<style is:global>
  .content-block--four {
    left: 30%;
    bottom: 10%;
  }
</style>

不過,內容區塊的一般樣式會與元件程式碼放在一起。

.content-block {
  background: hsl(0deg 0% 0% / 70%);
  color: var(--gray-0);
  border-radius:  min(3vh, var(--size-4));
  padding: clamp(0.75rem, 2vw, 1.25rem);
  display: grid;
  gap: var(--size-2);
  position: absolute;
  cursor: pointer;
  width: 50%;
}

對我們貓來說,這是一項互動功能,本專案就是其中之一。這個簡短範例說明瞭我們如何使用您建立的共用 ViewTimeline。

大致上,我們的 Owl 元件會匯入部分 SVG,並使用 Astro 的 Fragment 內嵌。

---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />

貓頭鷹的樣式會與元件程式碼共置。

.owl {
  width: 34%;
  left: 10%;
  bottom: 34%;
}

還有一項額外的樣式定義了貓頭鷹的 transform 行為。

.owl__owl {
  transform-origin: 50% 100%;
  transform-box: fill-box;
}

使用 transform-box 會影響 transform-origin。使其與 SVG 中的物件定界框相對。貓頭鷹從正下方向上擴充,因此使用 transform-origin: 50% 100%

有趣的部分,我們會將貓頭鷹與下列其中一個 ViewTimeline 連結起來:

const setUpOwl = () => {
   const owl = document.querySelector('.owl__owl');

   owl.animate([
     {
       translate: '0% 110%',
     },
     {
       translate: '0% 10%',
     },
   ], {
     timeline: CHROMETOBER_TIMELINES[1],
     delay: { phase: "enter", percent: CSS.percent(80) },
     endDelay: { phase: "enter", percent: CSS.percent(90) },
     fill: 'both' 
   });
 }

 if (window.matchMedia('(prefers-reduced-motion: no-preference)').matches)
   setUpOwl()

在這個程式碼區塊中,我們會執行下列兩項操作:

  1. 檢查使用者的動作偏好設定。
  2. 如果他們沒有偏好,請連結貓頭鷹動畫來捲動。

至於第二部分,貓頭鷹會使用 Web Animation API 在 y 軸上動畫。系統會使用個別轉換屬性 translate,並連結至一個 ViewTimeline。與 CHROMETOBER_TIMELINES[1] 透過 timeline 資源連結。這是系統為頁面轉彎而產生的 ViewTimeline。這會使用 enter 階段將貓頭鷹的動畫連結至頁面轉彎。定義當網頁轉了 80% 時,就可以開始移動貓頭鷹。貓頭鷹應該在 90% 時完成翻譯。

書籍功能

現在,您已瞭解建構頁面的方法和專案架構的運作方式。包括讓捐款者在指定的網頁或功能上參與其中。書中的許多功能都有和翻頁的動畫連結,例如一隻飛進/逃出的棒子。

其中也包含採用 CSS 動畫的元素。

內容區塊在書中後,你該發揮創意運用其他功能。這讓各位有機會進行一些不同的互動,並嘗試不同的實作方式。

保持回應靈敏

回應式可視區域單元會調整書籍及其功能的大小。不過,維持字型的回應式是個有趣的挑戰。這裡很適合使用容器查詢單元。但目前只在部分國家/地區推出。書籍大小已設定完成,因此不需要使用容器查詢。您可以透過 CSS calc() 產生內嵌容器查詢單元,以便調整字型大小。


.book-placeholder {
  --size: clamp(12rem, 72vw, 80vmin);
  --aspect-ratio: 360 / 504;
  --cqi: calc(0.01 * (var(--size) * (var(--aspect-ratio))));
}

.content-block h2 {
  color: var(--gray-0);
  font-size: clamp(0.6rem, var(--cqi) * 4, 1.5rem);
}

.content-block :is(p, a) {
  font-size: clamp(0.6rem, var(--cqi) * 3, 1.5rem);
}

南瓜燈夜色

有些人眼看著頁面背景,可能已經注意到使用了 <source> 元素。Una 積極尋求因應色彩配置偏好的互動。因此,背景支援有不同變化版本的淺色和深色模式。由於您可以使用 <picture> 元素與媒體查詢,因此這是提供兩種背景樣式的絕佳方法。<source> 元素會查詢色彩配置偏好設定,然後顯示適當的背景。

<picture>
  <source srcset={darkFront} media="(prefers-color-scheme: dark)" height="214" width="150">
  <img src={lightFront} class="page__background page__background--right" alt="" aria-hidden="true" height="214" width="150">
</picture>

您可以依據該色彩配置偏好設定導入其他變更。第 2 頁的南瓜會因應使用者的色彩配置偏好。使用的 SVG 檔具有代表火焰的圓圈,該圓形會在深色模式下放大並顯示動畫。

.pumpkin__flame,
 .pumpkin__flame circle {
   transform-box: fill-box;
   transform-origin: 50% 100%;
 }

 .pumpkin__flame {
   scale: 0.8;
 }

 .pumpkin__flame circle {
   transition: scale 0.2s;
   scale: 0;
 }

@media(prefers-color-scheme: dark) {
   .pumpkin__flame {
     animation: pumpkin-flicker 3s calc(var(--index, 0) * -1s) infinite linear;
   }

   .pumpkin__flame circle {
     scale: 1;
   }

   @keyframes pumpkin-flicker {
     50% {
       scale: 1;
     }
   }
 }

這個人像正在看著你嗎?

如果查看第 10 頁,可能會發現有些東西。你正在觀賞!當您瀏覽網頁時,人像的眼睛會跟著您的指針。訣竅在於將指標位置對應至翻譯值,再傳遞至 CSS。

const mapRange = (inputLower, inputUpper, outputLower, outputUpper, value) => {
   const INPUT_RANGE = inputUpper - inputLower
   const OUTPUT_RANGE = outputUpper - outputLower
   return outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0)
 }

這個程式碼會接收輸入和輸出範圍,並對應指定的值。例如,此用量應為 625 值。

mapRange(0, 100, 250, 1000, 50) // 625

以直向來說,輸入值是兩隻眼的中心點,加上或減去某個像素距離。輸出範圍是指眼睛可轉譯的像素大小。接著,X 軸或 Y 軸上的指標位置會以值的形式傳遞。為了在移動時提高眼睛的中心點,系統會重複眼睛。原始影片不會移動,資訊透明且僅供參考。

最後一種是結合在一起,並更新眼睛上的 CSS 自訂屬性值,讓眼睛移動。函式會與 window 繫結至 pointermove 事件。當火災時,系統會使用兩隻眼睛的邊界來計算中心點。接著,指標位置會對應到眼睛上設為自訂屬性值的值。

const RANGE = 15
const LIMIT = 80
const interact = ({ x, y }) => {
   // map a range against the eyes and pass in via custom properties
   const LEFT_EYE_BOUNDS = LEFT_EYE.getBoundingClientRect()
   const RIGHT_EYE_BOUNDS = RIGHT_EYE.getBoundingClientRect()

   const CENTERS = {
     lx: LEFT_EYE_BOUNDS.left + LEFT_EYE_BOUNDS.width * 0.5,
     rx: RIGHT_EYE_BOUNDS.left + RIGHT_EYE_BOUNDS.width * 0.5,
     ly: LEFT_EYE_BOUNDS.top + LEFT_EYE_BOUNDS.height * 0.5,
     ry: RIGHT_EYE_BOUNDS.top + RIGHT_EYE_BOUNDS.height * 0.5,
   }

   Object.entries(CENTERS)
     .forEach(([key, value]) => {
       const result = mapRange(value - LIMIT, value + LIMIT, -RANGE, RANGE)(key.indexOf('x') !== -1 ? x : y)
       EYES.style.setProperty(`--${key}`, result)
     })
 }

這些值傳送到 CSS 後,樣式即可視需要使用這些值。值得一提的是,只要使用 CSS clamp() 就能讓左右兩眼分別出現不同的行為。這樣能讓您的雙眼改變時,無需再次接觸 JavaScript。

.portrait__eye--mover {
   transition: translate 0.2s;
 }

 .portrait__eye--mover.portrait__eye--left {
   translate:
     clamp(-10px, var(--lx, 0) * 1px, 4px)
     clamp(-4px, var(--ly, 0) * 0.5px, 10px);
 }

 .portrait__eye--mover.portrait__eye--right {
   translate:
     clamp(-4px, var(--rx, 0) * 1px, 10px)
     clamp(-4px, var(--ry, 0) * 0.5px, 10px);
 }

施展咒語

如果看過第 6 頁,你覺得有特別之處嗎?本頁展現我們神奇魔法狐狸的設計。移動遊標時,可能會顯示自訂的遊標軌跡效果。這使用畫布動畫。<canvas> 元素會顯示在網頁內容的其餘部分上方,並加上 pointer-events: none。這表示使用者仍然可以點選下方的內容區塊。

.wand-canvas {
  height: 100%;
  width: 200%;
  pointer-events: none;
  right: 0;
  position: fixed;
}

就像直向會監聽 window 上的 pointermove 事件一樣,我們的 <canvas> 元素也是如此。但每次事件觸發時,系統都會建立物件來為 <canvas> 元素建立動畫。這些物件代表遊標路徑中使用的形狀。它們都有座標和隨機色調。

我們會再次使用先前提供的 mapRange 函式,因為我們可以使用該函式將指標差異對應至 sizerate。這些物件會儲存在陣列中,而在物件繪製至 <canvas> 元素時循環。每個物件的屬性都會告知 <canvas> 元素應繪製的位置。

const blocks = []
  const createBlock = ({ x, y, movementX, movementY }) => {
    const LOWER_SIZE = CANVAS.height * 0.05
    const UPPER_SIZE = CANVAS.height * 0.25
    const size = mapRange(0, 100, LOWER_SIZE, UPPER_SIZE, Math.max(Math.abs(movementX), Math.abs(movementY)))
    const rate = mapRange(LOWER_SIZE, UPPER_SIZE, 1, 5, size)
    const { left, top, width, height } = CANVAS.getBoundingClientRect()
    
    const block = {
      hue: Math.random() * 359,
      x: x - left,
      y: y - top,
      size,
      rate,
    }
    
    blocks.push(block)
  }
window.addEventListener('pointermove', createBlock)

如果是繪圖畫布,可以使用 requestAnimationFrame 建立迴圈。遊標追蹤記錄只有在頁面處於查看狀態時才會顯示。我們的 IntersectionObserver 會更新及判斷所瀏覽的頁面。如果使用者在檢視畫面中,物件會在畫布上顯示為圓形。

接著,我們會循環處理 blocks 陣列,並繪製小道的每個部分。每個頁框都會減少大小,並由 rate 變更物件位置。這會產生浮動效果及縮放效果。如果物件完全縮小,則物件會從 blocks 陣列中移除。

let wandFrame
const drawBlocks = () => {
   ctx.clearRect(0, 0, CANVAS.width, CANVAS.height)
  
   if (PAGE_SIX.className.indexOf('in-view') === -1 && wandFrame) {
     blocks.length = 0
     cancelAnimationFrame(wandFrame)
     document.body.removeEventListener('pointermove', createBlock)
     document.removeEventListener('resize', init)
   }
  
   for (let b = 0; b < blocks.length; b++) {
     const block = blocks[b]
     ctx.strokeStyle = ctx.fillStyle = `hsla(${block.hue}, 80%, 80%, 0.5)`
     ctx.beginPath()
     ctx.arc(block.x, block.y, block.size * 0.5, 0, 2 * Math.PI)
     ctx.stroke()
     ctx.fill()

     block.size -= block.rate
     block.y += block.rate

     if (block.size <= 0) {
       blocks.splice(b, 1)
     }

   }
   wandFrame = requestAnimationFrame(drawBlocks)
 }

如果網頁超出檢視畫面,系統就會移除事件監聽器,動畫影格循環也會取消。也會清除 blocks 陣列。

以下是遊標路徑的實際運作情形!

無障礙程度審查

讓使用者探索有趣的體驗是最好的辦法,但是如果使用者無法輕鬆使用,就不會有好處。Adam 在這個領域的專業知識,能協助 Chrometober 在推出無障礙工具前做好萬全準備。

涵蓋區域包括:

  • 確認使用的 HTML 是語意正確的。包括適當的地標元素,例如書籍的 <main>、每個內容區塊都使用 <article> 元素,以及加入縮寫字的 <abbr> 元素。展望未來,就在於讓閱聽人更容易理解書籍的內容。運用標題和連結,方便使用者瀏覽。使用網頁清單時,也代表輔助技術會朗讀出網頁數量。
  • 確認所有圖片都使用適當的 alt 屬性。如有必要,內嵌 SVG 中就會有 title 元素。
  • 使用 aria 屬性改善服務體驗。使用 aria-label 做為網頁及其側邊,就能向使用者傳達他們所在的頁面。「閱讀完整內容」連結上的 aria-describedBy 會傳達內容區塊的文字。這樣使用者就能更清楚地瞭解連結將導向何處。
  • 在內容區塊的主題上,使用者可以點選整張資訊卡,而不只是「閱讀完整內容」連結。
  • 使用 IntersectionObserver 追蹤之前顯示的網頁。這麼做有許多好處,而不只是與成效有關。未顯示的頁面會暫停播放動畫或互動。但這些網頁也已套用 inert 屬性。也就是說,螢幕閱讀器使用者能夠像視障使用者一樣瀏覽相同內容。焦點仍會保留在檢視畫面中,使用者無法將焦點移至其他頁面。
  • 最後,我們使用媒體查詢來尊重使用者的運動偏好。

以下是評論的螢幕截圖,突顯部分措施。

元素皆被標識為整本書籍的內容,表示該書籍應成為輔助科技使用者找到的主要地標。螢幕截圖中可以看到更多細節。" width="800" height="465">

開啟 Chrometober 書籍的螢幕截圖。使用者介面的各個方面都有顯示綠色外框的方塊,用來說明預定的無障礙功能,以及該網頁提供的使用者體驗。舉例來說,圖片內含替代文字。另一個範例是無障礙標籤,用於宣告未顯示檢視的網頁是無法聲明的。螢幕截圖中會列出更多細節。

我們的經驗教訓

Chrometober 背後的動機不僅是要展示社群中的網頁內容,我們也能藉此測試捲動式動畫 API Polyfill。

我們在紐約舉辦團隊高峰會時,除了安排時間對談,希望藉此測試專案並解決棘手的問題。團隊的貢獻內容非常珍貴。也是這個好機會,我們可以先列出需要處理的所有事情,然後才正式上線。

CSS、UI 和開發人員工具團隊坐在會議室中,Una 站在白板上,在便利貼上蓋上。其他團隊成員坐在桌前,展示點心和筆記型電腦。

例如,在測試裝置上測試書籍會導致轉譯問題。我們的書籍無法在 iOS 裝置上正常顯示。可視區域單元大小為頁面大小,但如果有凹口,影響書籍內容。解決方法是在 meta 可視區域中使用 viewport-fit=cover

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

這個工作階段也引發了 API polyfill 的問題。Bramus 會在 polyfill 存放區中提出這些問題。隨後他發現這些問題的解決方法,並將其合併為 polyfill。舉例來說,這個提取要求透過將快取新增至 polyfill 中,藉此提高效能。

螢幕截圖顯示在 Chrome 中開啟的示範畫面。開發人員工具隨即開啟,並顯示基準效能評估資料。

螢幕截圖顯示在 Chrome 中開啟的示範畫面。開發人員工具隨即開啟,顯示效能評估的改進。

這樣就大功告成了!

這個專案很有趣,能創造出奇特的捲動體驗,凸顯社群的精彩內容。這不僅有助於測試 polyfill,也可以提供意見回饋給工程團隊,協助改善 polyfill。

Chrometober 2022 體驗精彩回顧,

希望您會喜歡!你最喜歡什麼功能?歡迎透過推文告訴我們!

她手上拿著 Chrometober 角色的貼紙。

如果看到我們參加活動,你或許還能索取相關團隊的貼紙。

主頁橫幅由 David MenidreyUnsplash 網站上提供