楽しいハロウィーン テクニックを共有するスクロール ブックが Chrometober に登場した経緯をご紹介します。
今年は、Designcember に続いて、コミュニティと Chrome チームのウェブ コンテンツをハイライトして共有する方法として、Chrometober を構築しました。昨年の Designcember ではコンテナ クエリの使用方法を紹介しましたが、今年は CSS スクロールリンク アニメーション API を紹介しています。
スクロールする書籍の体験については、web.dev/chrometober-2022 をご覧ください。
概要
このプロジェクトの目標は、スクロールリンク アニメーション API を活用した風変わりなエクスペリエンスを提供することです。ただし、気まぐれなデザインでありながら、応答性とアクセスしやすさも必要でした。このプロジェクトは、現在開発中の API ポリフィルを試すうえでも、さまざまな手法やツールを組み合わせて試すうえでも、非常に有用な方法でした。ハロウィーン テーマでお届けします。
チームの構造は次のとおりです。
- Tyler Reed: イラストとデザイン
- Jhey Tompkins: アーキテクチャとクリエイティブのリードレディ
- Una Kravets: プロジェクト リーダー
- Bramus Van Damme: サイト コントリビューター
- Adam Argyle: ユーザー補助機能の審査
- Aaron Forinton: コピーライティング
スクロール テリング エクスペリエンスの下書きを作成する
Chrometober のアイデアは、2022 年 5 月に行われた最初のチーム オフサイトで生まれました。落書きのコレクションから、ユーザーがストーリーボードをスクロールできる方法を思いつきました。ビデオゲームにヒントを得て、墓地や幽霊屋敷などのシーンをスクロールするエクスペリエンスを検討しました。
初めての Google プロジェクトを思いがけない方向に自由に進めることができ、とてもワクワクしました。これは、ユーザーがコンテンツを操作する方法の初期プロトタイプです。
ユーザーが横方向にスクロールすると、ブロックが回転して拡大します。しかし、あらゆるサイズのデバイスのユーザーにこのエクスペリエンスを快適に提供できるか懸念したため、このアイデアは見送ることにしました。代わりに、過去に作ったデザインに傾倒しました。2020 年、私は幸運にも GreenSock の ScrollTrigger を使用してリリース デモを作成できました。
私が作成したデモの一つは、スクロールするとページがめくれる 3D-CSS ブックでした。これは、Chrometober で実現したいものに非常に適していると感じました。スクロールリンク アニメーション API は、その機能を完全に置き換えることができます。scroll-snap
とも連携して動作します。
このプロジェクトのイラストレーターである Tyler Reed は、アイデアが変わるたびにデザインを変更してくれました。Tyler は、投げかけられる創造的なアイデアをすべて取り入れ、それを実現する素晴らしい仕事をしました。一緒にアイデアを出し合うのはとても楽しかったです。機能が分離されたブロックに分割されることが、この仕組みの大きな特徴です。こうすることで、シーンを構成し、実体化したものを選択して選ぶことができました。
主なアイデアは、ユーザーが書籍を読み進めていくと、コンテンツのブロックにアクセスできるようにすることです。また、エクスペリエンスに組み込まれたイースター エッグなど、ちょっとした遊び心のある要素も含まれていました。たとえば、幽霊屋敷の肖像画がユーザーのカーソルを追いかけるように目が動いたり、メディア クエリによってトリガーされる繊細なアニメーションが表示されたりしました。これらのアイデアと機能は、スクロール時にアニメーション化されます。初期のアイデアは、ユーザーがスクロールすると、上昇して X 軸に沿って移動するゾンビ バニーでした。
API の概要
個々の機能やイースター エッグを使って遊ぶには、まず書籍が必要でした。そこで、この機会に、新しい CSS スクロールリンク アニメーション API の機能セットをテストすることにしました。現在、スクロールリンク アニメーション API はどのブラウザでもサポートされていません。ただし、API の開発中に、インタラクション チームのエンジニアは ポリフィルの開発に取り組んでいました。これにより、開発中に API の形状をテストできます。つまり、この API は現在から使用できます。このような楽しいプロジェクトは、試験運用版の機能を試したり、フィードバックを提供したりするのに最適な場所です。調査結果と、Google が提供できたフィードバックについては、この記事の後半をご覧ください。
概要として、この API を使用すると、アニメーションをスクロールにリンクできます。スクロール時にアニメーションをトリガーすることはできません。これは後で追加できます。スクロールにリンクされたアニメーションも、主に次の 2 つのカテゴリに分類されます。
- スクロール位置に反応するものです。
- スクロール コンテナ内の要素の位置に反応するアニメーション。
後者を作成するには、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 軸方向に移動して、書籍のように動作するようにしています。
すべてを組み合わせる
書籍の仕組みを理解できたので、タイラーのイラストを生き生きとさせる作業に集中できました。
天体写真
チームは 2021 年に Designcember で Astro を使用しており、私は 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;
}
ページ コンテンツ
ページの作成方法を見てみましょう。3 ページ目には、木から飛び出すフクロウが描かれています。
構成で定義されているように、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>
ページは基本的にアトミックです。特徴の集合から構築されます。ページ 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 の使用方法を示す、小さな例として適しています。
概要としては、フクロウ コンポーネントは SVG をインポートし、Astro の Fragment を使用してインライン化します。
---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />
また、フクロウの配置スタイルはコンポーネント コードと同じ場所にあります。
.owl {
width: 34%;
left: 10%;
bottom: 34%;
}
フクロウの transform
動作を定義する追加のスタイル設定が 1 つあります。
.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()
このコードブロックでは、次の 2 つのことを行います。
- お客様のモーション設定を確認します。
- 設定がない場合は、フクロウのアニメーションをスクロールにリンクします。
2 番目の部分では、Web Animations API を使用してフクロウが y 軸でアニメーション化されます。個々の変換プロパティ translate
が使用され、1 つの 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>
要素ではメディア クエリを使用できるため、2 つの背景スタイルを指定できます。<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 を確認すると、何かに気付くかもしれません。監視中です。ページを移動すると、ポートレートの目の動きがポインタに追従します。ここでのポイントは、ポインタの位置を translate 値にマッピングし、それを 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
関数は、ポインタのデルタを 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
配列もクリアされます。
カーソル トレイルの動作は次のとおりです。
ユーザー補助機能の審査
探索する楽しいエクスペリエンスを作成することは良いことですが、ユーザーがアクセスできない場合は意味がありません。アダムのこの分野に関する専門知識は、リリース前の Chrometober のユーザー補助に関する審査の準備に非常に役立ちました。
主な内容は次のとおりです。
- 使用した HTML がセマンティックであることを確認します。これには、書籍の
<main>
などの適切なランドマーク要素、コンテンツ ブロックごとに<article>
要素を使用する、頭字語が導入される場所に<abbr>
要素を使用するなど、書籍の作成時に先を見据えて対応することで、よりアクセスしやすくなりました。見出しとリンクを使用すると、ユーザーが簡単に移動できるようになります。ページにリストを使用すると、ページ数が支援技術によって読み上げられます。 - すべての画像で適切な
alt
属性が使用されていることを確認します。インライン SVG の場合、必要な場所にtitle
要素が存在します。 - エクスペリエンスを向上させる場所で
aria
属性を使用する。ページとその側面にaria-label
を使用すると、ユーザーに現在どのページにいるのかを通知できます。[続きを読む] リンクでaria-describedBy
を使用すると、コンテンツ ブロックのテキストが伝えられます。これにより、リンク先が不明確な状態を解消できます。 - コンテンツ ブロックについては、[続きを読む] リンクだけでなく、カード全体をクリックできるようになりました。
IntersectionObserver
を使用して、表示中のページを追跡する方法は前述しました。これには、パフォーマンスに関連するだけでなく、多くのメリットがあります。表示されていないページでは、アニメーションや操作が一時停止されます。ただし、これらのページにはinert
属性も適用されています。つまり、スクリーン リーダーを使用しているユーザーは、視覚に障がいのないユーザーと同じコンテンツを閲覧できます。フォーカスは表示されているページ内に残り、タブキーで別のページに移動することはできません。- 最後に、メディアクエリを使用して、ユーザーのモーション設定を尊重します。
以下は、実施されている対策の一部をハイライトした審査のスクリーンショットです。
要素が書籍全体として識別されている場合、これは支援技術のユーザーが見つけるためのメインのランドマークである必要があります。詳しくはスクリーンショットを参照してください。」 width="800" height="465">
振り返り
Chrometober の目的は、コミュニティのウェブ コンテンツをハイライトすることだけでなく、開発中のスクロールリンク アニメーション API ポリフィルを試す機会にもなりました。
ニューヨークで開催されたチーム サミットでは、プロジェクトをテストし、発生した問題に対処するためのセッションを設定しました。チームの貢献は非常に大きかったです。また、本稼働前に対処すべきすべてのことをリストアップする絶好の機会でもありました。
たとえば、デバイスで書籍をテストしたところ、レンダリングに関する問題が発生しました。書籍が iOS デバイスで想定どおりにレンダリングされない。ビューポート ユニットはページのサイズを設定しますが、ノッチがある場合は書籍に影響します。この問題を解決するには、meta
ビューポートで viewport-fit=cover
を使用しました。
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
このセッションでは、API ポリフィルに関する問題もいくつか提起されました。これらの問題は、Bramus がポリフィル リポジトリで報告しました。その後、これらの問題の解決策を見つけ、ポリフィルに統合しました。たとえば、この pull リクエストでは、ポリフィルの一部にキャッシュを追加することでパフォーマンスが向上しました。
これで作業は完了です。
このプロジェクトは本当に楽しく、コミュニティの素晴らしいコンテンツを際立たせる、風変わりなスクロール エクスペリエンスを実現できました。また、ポリフィルのテストや、ポリフィルの改善に役立つフィードバックの提供にも役立っています。
Chrometober 2022 は終了しました。
ご視聴いただきありがとうございました。お気に入りの機能は何ですか?ツイートして教えてください。
イベントにいらっしゃった際に、チームメンバーからステッカーを受け取ることもできます。
ヒーロー写真: David Menidrey(Unsplash)