這次的 Chrometober 活動,我們透過捲動式書籍分享有趣又恐怖的訣竅。
繼 Designcember 之後,今年我們想為大家打造 Chrometober,以此方式精選並分享社群和 Chrome 團隊的網路內容。去年的 Designcember 展示了容器查詢的使用方式,今年則會介紹 CSS 捲動連結動畫 API。
如要查看捲動書籍體驗,請前往 web.dev/chrometober-2022。
總覽
這項專案的目標是提供與捲動連結動畫 API 相關的奇妙體驗。不過,除了有趣之外,這項體驗也必須具備即時回應和易用性。這個專案也是測試正在開發中的 API polyfill 的絕佳方式,同時也能嘗試不同的技術和工具組合。讓所有元素都套用歡樂的萬聖節主題!
我們的團隊結構如下所示:
- Tyler Reed:插圖和設計
- Jhey Tompkins:架構和創意領導
- Una Kravets:專案負責人
- Bramus Van Damme:網站貢獻者
- Adam Argyle:無障礙審查
- 亞倫 Forinton:文案
編寫捲動式說書體驗草稿
Chrometober 的靈感在 2022 年 5 月,從第一個團隊異地激盪開始。一系列塗鴉包含許多塗鴉方式,讓我們思考了使用者可以透過何種方式在分鏡腳本中捲動瀏覽。我們受到電玩遊戲的啟發,考慮透過墓地和鬧鬼屋等場景,提供捲動體驗。
我很高興能擁有創作自由,將我的第一個 Google 專案帶往意想不到的方向。這是使用者瀏覽內容的早期原型。
使用者往側捲動時,方塊就會旋轉和縮小。但考量到如何為各種尺寸裝置的使用者提供優質體驗,我決定放棄這個想法。相反地,我傾向於採用過去設計的內容。2020 年,我很幸運地能使用 GreenSock 的 ScrollTrigger 建構發布版的示範。
我建立的示範模式之一是 3D-CSS 書籍,當你捲動瀏覽頁面時,頁面就能隨之轉動,對於 Chrometober 的理想選擇更是合適。捲動連結動畫 API 是這項功能的完美替代方案。您會發現,它也能與 scroll-snap
搭配使用。
Tyler Reed 是本專案的插畫師,他很擅長根據我們變更的想法調整設計。Tyler 非常出色地將所有創意構想付諸實現。一起集思廣益的過程非常有趣。我們希望這項功能能以分開的區塊來呈現,這樣一來,我們就能將這些元素組合成場景,然後挑選要讓哪些元素動起來。
主要概念是,使用者在閱讀書籍時,可以存取內容區塊。他們也可以與一些有趣的內容互動,包括我們在體驗中加入的復活節彩蛋,例如鬧鬼屋中的肖像畫,眼睛會跟著你的游標移動,或是由媒體查詢觸發的細微動畫。這些構想和功能會在捲動時顯示動畫效果。我們最初的想法是,在使用者捲動畫面時,讓殭屍兔子沿著 x 軸上升和平移。
熟悉 API
在開始玩弄個別功能和復活節彩蛋之前,我們需要一本書。因此,我們決定利用這個機會,測試新推出的 CSS 捲動連結動畫 API 的功能組合。目前,所有瀏覽器都不支援捲動連結動畫 API。不過,在開發 API 的同時,互動團隊的工程師也一直在進行polyfill 的開發工作。方便您在 API 開發時測試其形狀。也就是說,我們今天就能使用這個 API,而這類有趣的專案通常是測試實驗功能並提供意見回饋的好地方。本文後半段會說明我們瞭解到哪些資訊,以及我們提供的意見回饋。
整體來說,您可以使用這個 API 連結動畫來捲動。請注意,您無法在捲動時觸發動畫,這項功能可能會在日後推出。捲動連結動畫也分為兩大類別:
- 回應捲動位置的事件。
- 回應元素在捲動容器中的位置。
如要建立後者,我們會使用透過 animation-timeline
屬性套用的 ViewTimeline
。
以下是使用 ViewTimeline
在 CSS 中的樣式範例:
.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-delay
和 animation-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」就會在進入可視區域時放大,觸發「Spinner」的旋轉動作。
實驗結果顯示,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 Animations 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 進行 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 檔案,但頂端有類似前置資料的程式碼圍欄。這樣一來,我們就能匯入其他元件。第三頁的元件如下所示:
---
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 的開發過程有許多功能。第三頁包含內容區塊和互動式貓頭鷹,因此每個項目都有一個元件。
內容區塊是書籍內容的連結。這些也由設定物件驅動。
{
"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。
大致來說,我們的貓頭鷹元件會匯入部分 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()
在這個程式碼區塊中,我們會進行以下兩項作業:
- 檢查使用者的動作偏好設定。
- 如果沒有偏好設定,請連結貓頭鷹的動畫,讓畫面捲動。
在第二部分,貓頭鷹會使用 Web Animations API 在 y 軸上顯示動畫。使用個別的轉換屬性 translate
,並連結至一個 ViewTimeline
。這個設定檔是透過 timeline
屬性連結至 CHROMETOBER_TIMELINES[1]
。這是為了翻頁而產生的 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 自訂屬性值,讓眼睛可以移動。函式會繫結至 pointermove
事件,以便對應 window
。觸發此事件時,系統會使用每個眼睛的邊界來計算中心點。接著,系統會將指標位置對應至設為眼睛自訂屬性值的值。
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);
}
施放咒語
如果你查看第六頁,是否覺得很著迷?本頁面介紹我們超棒的魔法狐狸。移動遊標時,系統可能會顯示自訂的遊標軌跡效果。這會使用畫布動畫。<canvas>
元素位於頁面其餘內容上方,並顯示 pointer-events: none
。也就是說,使用者仍可點選下方的內容區塊。
.wand-canvas {
height: 100%;
width: 200%;
pointer-events: none;
right: 0;
position: fixed;
}
就像肖像會在 window
上監聽 pointermove
事件一樣,<canvas>
元素也會這麼做。不過,每次事件觸發時,我們都會建立物件,以便在 <canvas>
元素上顯示動畫。這些物件代表遊標軌跡中使用的形狀。這類圖像有座標和隨機色調。
我們再次使用先前的 mapRange
函式,因為我們可以使用該函式將指標差異值對應至 size
和 rate
。物件會儲存在陣列中,當物件繪製到 <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
屬性。對於內嵌可擴充向量圖形,title
元素會在必要時出現。 - 使用
aria
屬性改善使用體驗。網頁和網頁採用aria-label
可向使用者傳達目前所在的頁面。在「閱讀完整內容」連結上使用aria-describedBy
,可傳達內容區塊的文字。這樣一來,使用者就不會對連結的目的地感到困惑。 - 關於內容區塊,使用者可以點選整張資訊卡,而非僅點選「閱讀完整內容」連結。
- 先前提到,您可以使用
IntersectionObserver
追蹤哪些網頁正在瀏覽。這麼做不只對效能有益,系統會暫停所有不在視窗中的動畫或互動。但這些網頁也套用了inert
屬性。換句話說,使用螢幕閱讀器的使用者就能和視障使用者瀏覽相同的內容。焦點會保留在目前顯示的頁面中,使用者無法切換至其他頁面。 - 最後,我們會使用媒體查詢,以尊重使用者對動畫的偏好設定。
以下是審查結果的螢幕截圖,其中標示了我們採取的部分措施。
元素,表示這應是輔助技術使用者可找到的主要地標。詳情請參閱螢幕截圖。" width="800" height="465">
我們的經驗教訓
舉辦 Chrometober 的目的,不僅是為了推廣社群網站內容,也希望能測試目前正在開發中的捲動連結動畫 API polyfill。
我們在紐約團隊高峰會期間安排時間進行訪談,測試這項專案並解決相關問題。團隊的貢獻非常寶貴。這也是列出所有需要解決的問題,以便在正式上線前解決問題的絕佳機會。
舉例來說,在裝置上測試書籍時,會發生轉譯問題。我們的書籍無法在 iOS 裝置上正常顯示。可視區域的單元大小會調整頁面大小,但如果出現凹口,這會影響書籍。解決方法是在 meta
可視區域中使用 viewport-fit=cover
:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
這個工作階段也會引發 API polyfill 的相關問題。Bramus 會在 polyfill 存放區中提出這些問題。他隨後找到了這些問題的解決方案,並將這些解決方案合併至 polyfill。舉例來說,這個提取要求透過在部分 polyfill 中加入快取功能,提升了效能。
這樣就大功告成了!
這項專案的執行過程非常有趣,我們也打造出有趣的捲動體驗,讓使用者能瀏覽社群中精彩的內容。不僅如此,這項工具還可用於測試 polyfill,並向工程團隊提供意見回饋,協助改善 polyfill。
Chrometober 2022 活動結束。
希望你喜歡這堂課程!你最喜歡什麼功能?歡迎在 Twitter 上傳訊給我,告訴我們你的想法!
如果你在活動中看到我們,還可以索取團隊成員的貼紙。
主頁橫幅相片來源:Unsplash 上的 David Menidrey