Cara buku scroll dibuat untuk membagikan tips dan trik yang menyenangkan dan menakutkan pada Chrometober ini.
Setelah Designcember, kami ingin membuat Chrometober untuk Anda tahun ini sebagai cara untuk menyoroti dan membagikan konten web dari komunitas dan tim Chrome. Designcember menampilkan penggunaan Kueri Penampung, tetapi tahun ini kami menampilkan API animasi yang ditautkan scroll CSS.
Lihat pengalaman buku scroll di web.dev/chrometober-2022.
Ringkasan
Tujuan project ini adalah memberikan pengalaman unik yang menyoroti API animasi yang ditautkan scroll. Namun, meskipun bersifat aneh, pengalaman tersebut juga harus responsif dan mudah diakses. Project ini juga merupakan cara yang bagus untuk menguji polyfill API yang sedang dalam pengembangan aktif; serta mencoba berbagai teknik dan alat secara kombinasi. Semuanya dengan tema Halloween yang meriah.
Struktur tim kami terlihat seperti ini:
- Tyler Reed: Ilustrasi dan desain
- Jhey Tompkins: Pimpinan arsitektur dan kreatif
- Una Kravets: Project lead
- Bramus Van Damme: Kontributor situs
- Adam Argyle: Peninjauan aksesibilitas
- Aaron Forinton: Copywriting
Membuat draf pengalaman scrollytelling
Ide untuk Chrometober mulai mengalir di acara offsite tim pertama kami pada Mei 2022. Kumpulan coretan membuat kami memikirkan cara pengguna dapat men-scroll di sepanjang beberapa bentuk storyboard. Terinspirasi oleh video game, kami mempertimbangkan pengalaman scroll melalui berbagai latar seperti kuburan dan rumah hantu.
Sangat menyenangkan memiliki kebebasan kreatif untuk mengarahkan project Google pertama saya ke arah yang tidak terduga. Ini adalah prototipe awal tentang cara pengguna menavigasi konten.
Saat pengguna men-scroll ke samping, blok akan berputar dan diskalakan. Namun, saya memutuskan untuk tidak menggunakan ide ini karena khawatir bagaimana kami dapat membuat pengalaman ini menjadi luar biasa bagi pengguna di perangkat dari berbagai ukuran. Sebagai gantinya, saya lebih memilih desain sesuatu yang pernah saya buat sebelumnya. Pada tahun 2020, saya beruntung memiliki akses ke ScrollTrigger GreenSock untuk membuat demo rilis.
Salah satu demo yang saya buat adalah buku CSS 3D dengan halaman yang berputar saat Anda men-scroll, dan ini terasa jauh lebih sesuai dengan yang kami inginkan untuk Chrometober. API animasi yang ditautkan scroll adalah pengganti yang sempurna untuk fungsi tersebut. Fungsi ini juga berfungsi baik dengan scroll-snap
, seperti yang akan Anda lihat.
Illustrator kami untuk project ini, Tyler Reed, sangat mahir mengubah desain saat kami mengubah ide. Tyler melakukan pekerjaan yang luar biasa dengan mengambil semua ide kreatif yang diberikan kepadanya dan mewujudkannya. Sangat menyenangkan bertukar ide bersama. Bagian besar dari cara kerja yang kami inginkan adalah dengan membagi fitur menjadi blok terpisah. Dengan begitu, kita dapat menyusunnya menjadi sebuah scene, lalu memilih dan menentukan apa yang akan kita wujudkan.
Ide utamanya adalah, saat pengguna membaca buku, mereka dapat mengakses blok konten. Mereka juga dapat berinteraksi dengan sedikit keanehan, termasuk telur Paskah yang telah kami buat dalam pengalaman ini; misalnya, potret di rumah hantu, yang matanya mengikuti kursor Anda, atau animasi halus yang dipicu oleh kueri media. Ide dan fitur ini akan dianimasikan saat di-scroll. Ide awal adalah kelinci zombie yang akan naik dan menerjemahkan sepanjang sumbu x saat pengguna men-scroll.
Memahami API
Sebelum dapat mulai bermain dengan setiap fitur dan telur Paskah, kita memerlukan buku. Jadi, kami memutuskan untuk menjadikannya sebagai peluang untuk menguji kumpulan fitur untuk API animasi yang ditautkan scroll CSS yang baru muncul. API animasi yang ditautkan scroll saat ini tidak didukung di browser apa pun. Namun, saat mengembangkan API, engineer di tim interaksi telah mengerjakan polyfill. Hal ini memberikan cara untuk menguji bentuk API saat dikembangkan. Artinya, kita dapat menggunakan API ini sekarang, dan project seru seperti ini sering kali menjadi tempat yang tepat untuk mencoba fitur eksperimental, dan memberikan masukan. Cari tahu hal yang kami pelajari dan masukan yang dapat kami berikan, di bagian selanjutnya dalam artikel ini.
Pada tingkat tinggi, Anda dapat menggunakan API ini untuk menautkan animasi ke scroll. Perhatikan bahwa Anda tidak dapat memicu animasi saat men-scroll—ini adalah sesuatu yang dapat dilakukan nanti. Animasi yang ditautkan scroll juga terbagi menjadi dua kategori utama:
- Yang bereaksi terhadap posisi scroll.
- Yang bereaksi terhadap posisi elemen dalam penampung scroll-nya.
Untuk membuat yang terakhir, kita menggunakan ViewTimeline
yang diterapkan melalui properti animation-timeline
.
Berikut adalah contoh tampilan penggunaan ViewTimeline
di 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;
}
}
Kita membuat ViewTimeline
dengan view-timeline-name
dan menentukan sumbunya. Dalam contoh ini, block
merujuk pada block
logis. Animasi ditautkan ke scroll dengan properti animation-timeline
. animation-delay
dan animation-end-delay
(pada saat penulisan) adalah cara kita menentukan fase.
Fase ini menentukan titik saat animasi harus ditautkan sehubungan dengan posisi elemen dalam penampung scroll-nya. Dalam contoh ini, kita mengatakan mulai animasi saat elemen memasuki (enter 0%
) penampung scroll. Dan selesai setelah mencakup 50% (cover 50%
) penampung scroll.
Berikut adalah demo kami:
Anda juga dapat menautkan animasi ke elemen yang bergerak di area pandang. Anda dapat melakukannya dengan menetapkan animation-timeline
sebagai view-timeline
elemen. Hal ini bagus untuk skenario seperti animasi daftar. Perilaku ini mirip dengan cara Anda menganimasikan elemen saat masuk menggunakan 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;
}
}
Dengan ini,"Pemindahan" akan diskalakan saat memasuki area pandang, yang memicu rotasi "Spinner".
Yang saya temukan dari eksperimen adalah bahwa API berfungsi sangat baik dengan scroll-snap. Scroll-snap yang dikombinasikan dengan ViewTimeline
akan sangat cocok untuk mengambil gambar saat halaman buku dibalik.
Membuat prototipe mekanisme
Setelah beberapa kali bereksperimen, saya berhasil membuat prototipe buku berfungsi. Anda men-scroll secara horizontal untuk membalik halaman buku.
Dalam demo, Anda dapat melihat berbagai pemicu yang ditandai dengan batas putus-putus.
Markup-nya terlihat seperti ini:
<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>
Saat Anda men-scroll, halaman buku akan berputar, tetapi akan terbuka atau tertutup dengan cepat. Hal ini bergantung pada perataan scroll-snap pemicu.
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;
}
Kali ini, kita tidak menghubungkan ViewTimeline
di CSS, tetapi menggunakan Web Animations API di JavaScript. Hal ini memiliki manfaat tambahan karena dapat melakukan loop pada kumpulan elemen dan menghasilkan ViewTimeline
yang kita perlukan, bukan membuatnya satu per satu secara manual.
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);
Untuk setiap pemicu, kita membuat ViewTimeline
. Kemudian, kita menganimasikan halaman terkait pemicu menggunakan ViewTimeline
tersebut. Yang menautkan animasi halaman ke scroll. Untuk animasi, kita memutar elemen halaman pada sumbu y untuk memutar halaman. Kita juga menerjemahkan halaman itu sendiri pada sumbu z sehingga berperilaku seperti buku.
Menggabungkan semuanya
Setelah memahami mekanisme buku, saya dapat berfokus untuk mewujudkan ilustrasi Tyler.
Astro
Tim menggunakan Astro untuk Designcember pada tahun 2021 dan saya ingin menggunakannya lagi untuk Chrometober. Pengalaman developer yang dapat memecah berbagai hal menjadi komponen sangat cocok untuk project ini.
Buku itu sendiri adalah komponen. Ini juga merupakan kumpulan komponen halaman. Setiap halaman memiliki dua sisi dan memiliki latar belakang. Turunan sisi halaman adalah komponen yang dapat ditambahkan, dihapus, dan diposisikan dengan mudah.
Membuat buku
Saya ingin membuat blok mudah dikelola. Saya juga ingin mempermudah anggota tim lainnya untuk memberikan kontribusi.
Halaman pada tingkat tinggi ditentukan oleh array konfigurasi. Setiap objek halaman dalam array menentukan konten, latar belakang, dan metadata lainnya untuk halaman.
const pages = [
{
front: {
marked: true,
content: PageTwo,
backdrop: spreadOne,
darkBackdrop: spreadOneDark
},
back: {
content: PageThree,
backdrop: spreadTwo,
darkBackdrop: spreadTwoDark
},
aria: `page 1`
},
/* Obfuscated page objects */
]
Nilai ini akan diteruskan ke komponen Book
.
<Book pages={pages} />
Komponen Book
adalah tempat mekanisme scroll diterapkan dan halaman buku dibuat. Mekanisme yang sama dari prototipe digunakan; tetapi kita berbagi beberapa instance ViewTimeline
yang dibuat secara global.
window.CHROMETOBER_TIMELINES.push(viewTimeline);
Dengan cara ini, kita dapat membagikan linimasa untuk digunakan di tempat lain, bukan membuat ulang. Selengkapnya akan dibahas nanti.
Komposisi halaman
Setiap halaman adalah item daftar di dalam daftar:
<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>
Dan konfigurasi yang ditentukan akan diteruskan ke setiap instance Page
. Halaman menggunakan fitur slot Astro untuk menyisipkan konten ke setiap halaman.
<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>
Kode ini sebagian besar digunakan untuk menyiapkan struktur. Kontributor dapat mengerjakan sebagian besar konten buku tanpa harus menyentuh kode ini.
Latar belakang
Pergeseran kreatif ke buku membuat pemisahan bagian menjadi jauh lebih mudah, dan setiap lembar buku adalah adegan yang diambil dari desain asli.
Karena kita telah memutuskan rasio aspek untuk buku, latar belakang untuk setiap halaman dapat memiliki elemen gambar. Menetapkan elemen tersebut ke lebar 200% dan menggunakan object-position
berdasarkan sisi halaman akan menyelesaikan masalah.
.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;
}
Konten halaman
Mari kita lihat cara membuat salah satu halaman. Halaman ketiga menampilkan seekor burung hantu yang muncul di pohon.
Kolom ini diisi dengan komponen PageThree
, seperti yang ditentukan dalam konfigurasi. Ini adalah komponen Astro (PageThree.astro
). Komponen ini terlihat seperti file HTML, tetapi memiliki pagar kode di bagian atas yang mirip dengan bagian awal. Hal ini memungkinkan kita melakukan hal-hal seperti mengimpor komponen lain. Komponen untuk halaman ketiga terlihat seperti ini:
---
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>
Sekali lagi, halaman bersifat atomik. Aplikasi dibuat dari kumpulan fitur. Halaman ketiga menampilkan blok konten dan burung hantu interaktif, sehingga ada komponen untuk masing-masing.
Blok konten adalah link ke konten yang terlihat dalam buku. Hal ini juga didorong oleh objek konfigurasi.
{
"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
]
}
Konfigurasi ini akan diimpor jika blok konten diperlukan. Kemudian, konfigurasi blok yang relevan akan diteruskan ke komponen ContentBlock
.
<ContentBlock {...contentBlocks[3]} id="four" />
Di sini juga ada contoh cara menggunakan komponen halaman sebagai tempat untuk memosisikan konten. Di sini, blok konten diposisikan.
<style is:global>
.content-block--four {
left: 30%;
bottom: 10%;
}
</style>
Namun, gaya umum untuk blok konten ditempatkan bersama dengan kode komponen.
.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%;
}
Untuk owl, ini adalah fitur interaktif—salah satu dari banyak fitur dalam project ini. Ini adalah contoh kecil yang bagus untuk dipelajari yang menunjukkan cara menggunakan ViewTimeline bersama yang kita buat.
Pada tingkat tinggi, komponen owl kami mengimpor beberapa SVG dan menyisipkannya menggunakan Fragment Astro.
---
import { default as Owl } from '../Features/Owl.svg?raw'
---
<Fragment set:html={Owl} />
Selain itu, gaya untuk memosisikan owl kita berada di lokasi yang sama dengan kode komponen.
.owl {
width: 34%;
left: 10%;
bottom: 34%;
}
Ada satu bagian gaya tambahan yang menentukan perilaku transform
untuk owl.
.owl__owl {
transform-origin: 50% 100%;
transform-box: fill-box;
}
Penggunaan transform-box
memengaruhi transform-origin
. Hal ini membuatnya relatif terhadap kotak pembatas objek dalam SVG. Burung hantu diskalakan dari tengah bawah, sehingga penggunaan transform-origin: 50% 100%
.
Bagian yang menyenangkan adalah saat kita menautkan owl ke salah satu ViewTimeline
yang dihasilkan:
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()
Dalam blok kode ini, kita melakukan dua hal:
- Periksa preferensi gerakan pengguna.
- Jika tidak memiliki preferensi, tautkan animasi burung hantu untuk men-scroll.
Untuk bagian kedua, burung hantu dianimasikan pada sumbu y menggunakan Web Animations API. Setiap properti transformasi translate
digunakan, dan ditautkan ke satu ViewTimeline
. Properti ini ditautkan ke CHROMETOBER_TIMELINES[1]
melalui properti timeline
. Ini adalah ViewTimeline
yang dihasilkan untuk pergantian halaman. Tindakan ini akan menautkan animasi owl ke pergantian halaman menggunakan fase enter
. Ini menentukan bahwa, saat halaman diputar 80%, mulailah menggerakkan burung hantu. Pada 90%, burung hantu akan menyelesaikan terjemahannya.
Fitur buku
Sekarang Anda telah melihat pendekatan untuk membuat halaman dan cara kerja arsitektur project. Anda dapat melihat cara fitur ini memungkinkan kontributor untuk langsung bergabung dan mengerjakan halaman atau fitur pilihan mereka. Berbagai fitur dalam buku memiliki animasi yang ditautkan ke halaman buku yang dibalik; misalnya, kelelawar yang terbang masuk dan keluar saat halaman dibalik.
Halaman ini juga memiliki elemen yang didukung oleh animasi CSS.
Setelah blok konten ada dalam buku, ada waktu untuk berkreasi dengan fitur lainnya. Hal ini memberikan peluang untuk menghasilkan beberapa interaksi yang berbeda, dan mencoba berbagai cara untuk menerapkan berbagai hal.
Menjaga semuanya tetap responsif
Unit area pandang responsif menentukan ukuran buku dan fiturnya. Namun, menjaga font tetap responsif adalah tantangan yang menarik. Unit kueri penampung sangat cocok di sini. Namun, fitur ini belum didukung di semua tempat. Ukuran buku telah ditetapkan, sehingga kita tidak memerlukan kueri penampung. Unit kueri penampung inline dapat dibuat dengan calc()
CSS dan digunakan untuk ukuran font.
.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);
}
Labu bersinar di malam hari
Pengguna yang jeli mungkin telah melihat penggunaan elemen <source>
saat membahas latar belakang halaman sebelumnya. Una ingin memiliki interaksi yang bereaksi terhadap preferensi skema warna. Akibatnya, latar belakang mendukung mode terang dan gelap dengan varian yang berbeda. Karena Anda dapat menggunakan kueri media dengan elemen <picture>
, ini adalah cara yang bagus untuk menyediakan dua gaya latar belakang. Elemen <source>
membuat kueri untuk preferensi skema warna, dan menampilkan latar belakang yang sesuai.
<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>
Anda dapat memperkenalkan perubahan lain berdasarkan preferensi skema warna tersebut. Labu di halaman kedua bereaksi terhadap preferensi skema warna pengguna. SVG yang digunakan memiliki lingkaran yang mewakili api, yang diskalakan dan dianimasikan dalam mode gelap.
.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;
}
}
}
Apakah potret ini sedang melihat Anda?
Jika melihat halaman 10, Anda mungkin akan melihat sesuatu. Anda sedang ditonton! Mata potret akan mengikuti kursor Anda saat Anda bergerak di sekitar halaman. Triknya di sini adalah memetakan lokasi pointer ke nilai terjemahan, dan meneruskannya ke 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)
}
Kode ini mengambil rentang input dan output, serta memetakan nilai yang diberikan. Misalnya, penggunaan ini akan memberikan nilai 625.
mapRange(0, 100, 250, 1000, 50) // 625
Untuk potret, nilai input adalah titik tengah setiap mata, ditambah atau dikurangi beberapa jarak piksel. Rentang output adalah jumlah yang dapat diterjemahkan oleh mata dalam piksel. Kemudian, posisi pointer pada sumbu x atau y akan diteruskan sebagai nilai. Untuk mendapatkan titik tengah mata saat memindahkannya, mata akan diduplikasi. Gambar asli tidak bergerak, transparan, dan digunakan sebagai referensi.
Kemudian, Anda harus menggabungkannya dan memperbarui nilai properti kustom CSS pada mata agar mata dapat bergerak. Fungsi terikat dengan peristiwa pointermove
terhadap window
. Saat ini diaktifkan, batas setiap mata akan digunakan untuk menghitung titik tengah. Kemudian, posisi pointer dipetakan ke nilai yang ditetapkan sebagai nilai properti kustom pada mata.
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)
})
}
Setelah nilai diteruskan ke CSS, gaya dapat melakukan apa yang diinginkan dengan nilai tersebut. Bagian yang bagus di sini adalah menggunakan clamp()
CSS untuk membuat perilaku berbeda untuk setiap mata, sehingga Anda dapat membuat setiap mata berperilaku berbeda tanpa menyentuh JavaScript lagi.
.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);
}
Melakukan mantra
Jika Anda melihat halaman enam, apakah Anda merasa terpesona? Halaman ini menampilkan desain rubah ajaib yang fantastis. Jika menggerakkan kursor, Anda mungkin melihat efek jejak kursor kustom. Ini menggunakan animasi kanvas. Elemen <canvas>
berada di atas konten halaman lainnya dengan pointer-events: none
. Artinya, pengguna masih dapat mengklik blok konten di bawahnya.
.wand-canvas {
height: 100%;
width: 200%;
pointer-events: none;
right: 0;
position: fixed;
}
Sama seperti cara potret memproses peristiwa pointermove
di window
, begitu juga elemen <canvas>
. Namun, setiap kali peristiwa diaktifkan, kita membuat objek untuk dianimasikan pada elemen <canvas>
. Objek ini mewakili bentuk yang digunakan di jejak kursor. Objek ini memiliki koordinat dan hue acak.
Fungsi mapRange
dari sebelumnya digunakan lagi, karena kita dapat menggunakannya untuk memetakan delta pointer ke size
dan rate
. Objek disimpan dalam array yang di-loop saat objek digambar ke elemen <canvas>
. Properti untuk setiap objek memberi tahu elemen <canvas>
tempat objek harus digambar.
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)
Untuk menggambar ke kanvas, loop dibuat dengan requestAnimationFrame
. Jejak kursor hanya boleh dirender saat halaman terlihat. Kita memiliki IntersectionObserver
yang memperbarui dan menentukan halaman mana yang ditampilkan. Jika halaman terlihat, objek akan dirender sebagai lingkaran di kanvas.
Kemudian, kita akan melakukan loop pada array blocks
dan menggambar setiap bagian jalur. Setiap frame mengurangi ukuran dan mengubah posisi objek dengan rate
. Hal ini menghasilkan efek jatuh dan penskalaan. Jika objek menyusut sepenuhnya, objek akan dihapus dari array 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)
}
Jika halaman tidak terlihat, pemroses peristiwa akan dihapus dan loop frame animasi akan dibatalkan. Array blocks
juga dihapus.
Berikut adalah jejak kursor yang sedang berjalan.
Peninjauan aksesibilitas
Menyajikan pengalaman yang menyenangkan untuk dijelajahi memang bagus, tetapi tidak akan berguna jika tidak dapat diakses oleh pengguna. Keahlian Adam di bidang ini terbukti sangat berharga dalam mempersiapkan Chrometober untuk peninjauan aksesibilitas sebelum rilis.
Beberapa area penting yang dibahas:
- Memastikan bahwa HTML yang digunakan bersifat semantik. Hal ini mencakup hal-hal seperti elemen penanda yang sesuai seperti
<main>
untuk buku; serta penggunaan elemen<article>
untuk setiap blok konten, dan elemen<abbr>
tempat akronim diperkenalkan. Berpikir ke depan saat buku dibuat membuat semuanya lebih mudah diakses. Penggunaan judul dan link memudahkan pengguna untuk menavigasi. Penggunaan daftar untuk halaman juga berarti jumlah halaman diumumkan oleh teknologi pendukung. - Memastikan semua gambar menggunakan atribut
alt
yang sesuai. Untuk SVG inline, elementitle
ada jika diperlukan. - Menggunakan atribut
aria
yang meningkatkan pengalaman. Penggunaanaria-label
untuk halaman dan sisinya memberi tahu pengguna halaman mana yang sedang dibuka. Penggunaanaria-describedBy
pada link "Baca selengkapnya" menyampaikan teks blok konten. Hal ini menghilangkan ambiguitas tentang tempat link akan mengarahkan pengguna. - Untuk blok konten, Anda dapat mengklik seluruh kartu, bukan hanya link "Baca selengkapnya".
- Penggunaan
IntersectionObserver
untuk melacak halaman mana yang ditampilkan telah dibahas sebelumnya. Hal ini memiliki banyak manfaat yang tidak hanya terkait performa. Animasi atau interaksi di halaman yang tidak terlihat akan dijeda. Namun, halaman ini juga memiliki atributinert
yang diterapkan. Artinya, pengguna yang menggunakan pembaca layar dapat menjelajahi konten yang sama dengan pengguna yang dapat melihat. Fokus tetap berada dalam halaman yang sedang dilihat dan pengguna tidak dapat beralih ke halaman lain dengan tab. - Terakhir, kita menggunakan kueri media untuk mengikuti preferensi pengguna terkait gerakan.
Berikut adalah screenshot dari peninjauan yang menyoroti beberapa tindakan yang diterapkan.
diidentifikasi sebagai di sekitar seluruh buku, yang menunjukkan bahwa elemen tersebut harus menjadi penanda utama yang dapat ditemukan oleh pengguna teknologi pendukung. Selengkapnya diuraikan dalam screenshot." width="800" height="465">
Yang kami pelajari
Motivasi di balik Chrometober tidak hanya untuk menyoroti konten web dari komunitas, tetapi juga merupakan cara bagi kami untuk menguji polyfill API animasi yang ditautkan scroll yang sedang dalam pengembangan.
Kami menyisihkan waktu saat mengikuti summit tim di New York untuk menguji project dan mengatasi masalah yang muncul. Kontribusi tim sangat berharga. Ini juga merupakan peluang bagus untuk mencantumkan semua hal yang perlu ditangani sebelum kami dapat melakukan live streaming.
Misalnya, menguji buku di perangkat menimbulkan masalah rendering. Buku kami tidak akan dirender seperti yang diharapkan di perangkat iOS. Unit area pandang menentukan ukuran halaman, tetapi jika ada notch, hal ini akan memengaruhi buku. Solusinya adalah menggunakan viewport-fit=cover
di area pandang meta
:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Sesi ini juga menimbulkan beberapa masalah terkait polyfill API. Bramus melaporkan masalah ini di repositori polyfill. Kemudian, ia menemukan solusi untuk masalah tersebut dan menggabungkannya ke dalam polyfill. Misalnya, permintaan pull ini meningkatkan performa dengan menambahkan penyimpanan dalam cache ke bagian polyfill.
Selesai.
Ini adalah project yang sangat menyenangkan untuk dikerjakan, yang menghasilkan pengalaman scroll yang unik dan menyoroti konten luar biasa dari komunitas. Tidak hanya itu, ini sangat bagus untuk menguji polyfill, serta memberikan masukan kepada tim engineer untuk membantu meningkatkan polyfill.
Chrometober 2022 telah berakhir.
Semoga Anda menyukainya. Apa fitur favorit Anda? Kirim tweet kepada kami dan beri tahu kami.
Anda bahkan mungkin bisa mendapatkan beberapa stiker dari salah satu anggota tim jika bertemu kami di acara.
Foto Hero oleh David Menidrey di Unsplash