Membangun Chrometober!

Cara buku scroll diwujudkan untuk membagikan tips dan trik yang menyenangkan dan menakutkan di Chrometober ini.

Melanjutkan 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 scroll-linked CSS.

Lihat pengalaman buku scroll di web.dev/chrometober-2022.

Ringkasan

Tujuan project ini adalah untuk memberikan pengalaman yang unik dengan menyoroti Animation API yang ditautkan ke scroll. Namun, meskipun unik, pengalaman ini 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 bersamaan. Semuanya dengan tema Halloween yang meriah.

Struktur tim kami terlihat seperti ini:

Menyusun draf pengalaman scrollytelling

Ide untuk Chrometober mulai mengalir pada acara di luar kantor tim pertama kami pada Mei 2022. Kumpulan coretan membuat kami memikirkan cara pengguna dapat men-scroll beberapa bentuk storyboard. Terinspirasi oleh video game, kami mempertimbangkan pengalaman men-scroll adegan seperti kuburan dan rumah hantu.

Sebuah buku catatan tergeletak di meja dengan berbagai coretan dan sketsa yang terkait dengan project.

Saya senang memiliki kebebasan kreatif untuk mengarahkan project Google pertama saya ke arah yang tidak terduga. Ini adalah prototipe awal tentang cara pengguna menjelajahi konten.

Saat pengguna men-scroll ke samping, blok akan berputar dan diskalakan. Namun, saya memutuskan untuk tidak menggunakan ide ini karena khawatir tentang cara kami dapat membuat pengalaman ini bagus untuk pengguna di perangkat dengan berbagai ukuran. Sebaliknya, saya cenderung menggunakan desain 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 3D-CSS yang halamannya berbalik saat Anda men-scroll, dan ini terasa jauh lebih sesuai dengan yang kami inginkan untuk Chrometober. Scroll-linked animations API adalah pengganti yang sempurna untuk fungsi tersebut. Fitur ini juga berfungsi baik dengan scroll-snap, seperti yang akan Anda lihat.

Ilustrator kami untuk project ini, Tyler Reed, sangat hebat dalam mengubah desain seiring perubahan ide kami. Tyler melakukan pekerjaan yang luar biasa dalam menerima semua ide kreatif yang diberikan kepadanya dan mewujudkannya. Sangat menyenangkan berdiskusi bersama. Bagian penting dari cara kami ingin fitur ini berfungsi adalah dengan membagi fitur menjadi blok-blok yang terisolasi. Dengan begitu, kami dapat menyusunnya menjadi adegan, lalu memilih dan menentukan apa yang akan kami hidupkan.

Salah satu adegan komposisi yang menampilkan ular, peti mati dengan lengan yang keluar, rubah dengan tongkat sihir di kuali, pohon dengan wajah seram, dan gargoyle yang memegang lentera labu.

Ide utamanya adalah, saat pengguna membaca buku, mereka dapat mengakses blok konten. Mereka juga dapat berinteraksi dengan sentuhan imajinasi, termasuk easter egg yang telah kami bangun ke dalam pengalaman; 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 awalnya adalah kelinci zombie yang akan bangkit dan bergerak di sepanjang sumbu x saat pengguna men-scroll.

Mengenal API

Sebelum dapat mulai bermain dengan fitur individual dan Easter egg, kami memerlukan buku. Jadi, kami memutuskan untuk mengubahnya menjadi peluang untuk menguji set fitur untuk API animasi scroll-linked CSS yang baru muncul. API animasi yang ditautkan ke scroll saat ini tidak didukung di browser apa pun. Namun, saat mengembangkan API, para 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 yang menyenangkan seperti ini sering kali menjadi tempat yang bagus untuk mencoba fitur eksperimental, dan memberikan masukan. Cari tahu apa yang kami pelajari dan masukan yang dapat kami berikan, di bagian selanjutnya dalam artikel ini.

Secara umum, Anda dapat menggunakan API ini untuk menautkan animasi ke scroll. Perlu diperhatikan bahwa Anda tidak dapat memicu animasi saat men-scroll—ini adalah sesuatu yang dapat dilakukan nanti. Animasi yang ditautkan ke scroll juga terbagi dalam dua kategori utama:

  1. Yang bereaksi terhadap posisi scroll.
  2. Animasi yang bereaksi terhadap posisi elemen dalam container scroll-nya.

Untuk membuat yang terakhir, kita menggunakan ViewTimeline yang diterapkan melalui properti animation-timeline.

Berikut adalah contoh 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 sumbu untuknya. Dalam contoh ini, block mengacu pada block logis. Animasi ditautkan ke scroll dengan properti animation-timeline. animation-delay dan animation-end-delay (pada saat penulisan) adalah cara kami menentukan fase.

Fase ini menentukan titik tempat animasi harus ditautkan sehubungan dengan posisi elemen dalam container scroll-nya. Dalam contoh ini, kita akan memulai animasi saat elemen memasuki (enter 0%) container scroll. Dan selesai saat mencakup 50% (cover 50%) penampung scroll.

Berikut adalah demo yang sedang berjalan:

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 berguna untuk skenario seperti animasi daftar. Perilakunya 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,"Mover" akan diperbesar saat memasuki area pandang, sehingga memicu rotasi "Spinner".

Dari eksperimen yang saya lakukan, API ini berfungsi sangat baik dengan scroll-snap. Pengepasan posisi scroll yang dikombinasikan dengan ViewTimeline akan sangat cocok untuk mengepaskan posisi scroll saat membalik halaman dalam buku.

Membuat prototipe mekanika

Setelah beberapa kali bereksperimen, saya berhasil membuat prototipe buku yang 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 akan 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 dibalik, tetapi akan langsung terbuka atau tertutup. 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, yaitu dapat melakukan loop pada sekumpulan elemen dan menghasilkan ViewTimeline yang kita butuhkan, alih-alih 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, kami membuat ViewTimeline. Kemudian, kita menganimasikan halaman terkait pemicu menggunakan ViewTimeline tersebut. Tindakan ini akan menautkan animasi halaman ke scroll. Untuk animasi, kita memutar elemen halaman pada sumbu y untuk membalik halaman. Kami juga menerjemahkan halaman itu sendiri pada sumbu z sehingga berperilaku seperti buku.

Menggabungkan semuanya

Setelah memahami mekanisme buku ini, saya dapat berfokus untuk menghidupkan ilustrasi Tyler.

Astro

Tim menggunakan Astro untuk Designcember pada tahun 2021 dan saya ingin menggunakannya lagi untuk Chrometober. Pengalaman developer dalam memecah berbagai hal menjadi komponen sangat cocok untuk project ini.

Buku itu sendiri adalah sebuah komponen. Ini juga merupakan kumpulan komponen halaman. Setiap halaman memiliki dua sisi dan memiliki latar belakang. Turunan dari sisi halaman adalah komponen yang dapat ditambahkan, dihapus, dan diposisikan dengan mudah.

Membuat buku

Penting bagi saya untuk membuat blok yang mudah dikelola. Saya juga ingin memudahkan anggota tim lainnya untuk memberikan kontribusi.

Halaman pada tingkat tinggi ditentukan oleh array konfigurasi. Setiap objek halaman dalam array menentukan konten, tampilan latar, 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 */
]

Parameter ini 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 kami membagikan 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 membuatnya ulang. Hal ini akan dibahas lebih lanjut 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>

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 konten buku sebagian besar tanpa harus menyentuh kode ini.

Latar belakang

Perubahan kreatif ke arah buku membuat pembagian bagian menjadi jauh lebih mudah, dan setiap halaman buku adalah adegan yang diambil dari desain aslinya.

Ilustrasi halaman buku yang menampilkan pohon apel di pemakaman. Kuburan memiliki beberapa batu nisan dan ada kelelawar di langit di depan bulan besar.

Karena kami 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 berhasil.

.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 tiga menampilkan burung hantu yang muncul di pohon.

Elemen ini diisi dengan komponen PageThree, seperti yang ditentukan dalam konfigurasi. Ini adalah komponen Astro (PageThree.astro). Komponen ini terlihat seperti file HTML, tetapi memiliki pembatas kode di bagian atas yang mirip dengan frontmatter. Hal ini memungkinkan kita melakukan hal-hal seperti mengimpor komponen lain. Komponen untuk halaman tiga 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. Model ini dibuat dari kumpulan fitur. Halaman tiga menampilkan blok konten dan burung hantu interaktif, sehingga ada komponen untuk masing-masing elemen.

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 diimpor di tempat yang memerlukan blok konten. Kemudian, konfigurasi blok yang relevan akan diteruskan ke komponen ContentBlock.

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

Ada juga contoh di sini tentang cara kami 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 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%;
}

Sedangkan untuk burung hantu, ini adalah fitur interaktif—salah satu dari banyak fitur dalam project ini. Ini adalah contoh kecil yang bagus untuk dipelajari, yang menunjukkan cara kami menggunakan ViewTimeline bersama yang kami buat.

Secara umum, komponen burung hantu kita mengimpor beberapa SVG dan menyisipkannya menggunakan Fragment Astro.

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

Gaya untuk memosisikan burung hantu kita ditempatkan bersama dengan kode komponen.

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

Ada satu elemen gaya tambahan yang menentukan perilaku transform untuk burung hantu.

.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 menggunakan transform-origin: 50% 100%.

Bagian yang menyenangkan adalah saat kita menautkan burung hantu ke salah satu ViewTimeline yang kita buat:

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:

  1. Periksa preferensi gerakan pengguna.
  2. Jika mereka tidak memiliki preferensi, tautkan animasi burung hantu ke scroll.

Untuk bagian kedua, burung hantu dianimasikan pada sumbu y menggunakan Web Animations API. Properti transform individual translate digunakan, dan ditautkan ke satu ViewTimeline. Akun ini ditautkan ke CHROMETOBER_TIMELINES[1] melalui properti timeline. Ini adalah ViewTimeline yang dibuat untuk membalik halaman. Tindakan ini akan menautkan animasi burung hantu ke pergantian halaman menggunakan fase enter. Ini menentukan bahwa, saat halaman dibalik 80%, burung hantu akan mulai bergerak. Pada 90%, burung hantu harus menyelesaikan terjemahannya.

Fitur buku

Sekarang Anda telah melihat pendekatan untuk membuat halaman dan cara kerja arsitektur project. Anda dapat melihat bagaimana hal ini memungkinkan kontributor untuk masuk dan mengerjakan halaman atau fitur pilihan mereka. Berbagai fitur dalam buku ini memiliki animasi yang ditautkan ke pembalikan halaman buku; misalnya, kelelawar yang terbang masuk dan keluar saat halaman dibalik.

Halaman ini juga memiliki elemen yang didukung oleh animasi CSS.

Setelah blok konten dimasukkan ke dalam buku, ada waktu untuk berkreasi dengan fitur lainnya. Hal ini memberikan peluang untuk menghasilkan beberapa interaksi yang berbeda, dan mencoba berbagai cara untuk mengimplementasikan berbagai hal.

Menjaga aplikasi tetap responsif

Unit area pandang responsif mengukur buku dan fiturnya. Namun, menjaga font tetap responsif merupakan tantangan yang menarik. Unit kueri penampung cocok digunakan di sini. Namun, fitur ini belum didukung di semua tempat. Ukuran buku sudah ditetapkan, jadi 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

Mereka yang jeli mungkin telah memperhatikan penggunaan elemen <source> saat membahas latar belakang halaman sebelumnya. Una ingin memiliki interaksi yang bereaksi terhadap preferensi skema warna. Hasilnya, 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 backdrop. Elemen <source> mengkueri 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 dua bereaksi terhadap preferensi skema warna pengguna. SVG yang digunakan memiliki lingkaran yang merepresentasikan 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 mengawasi Anda?

Jika Anda melihat halaman 10, Anda mungkin akan melihat sesuatu. Anda sedang diawasi. Mata potret akan mengikuti pointer saat Anda bergerak di 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 inputnya adalah titik tengah setiap mata, ditambah atau dikurangi beberapa jarak piksel. Rentang output adalah seberapa banyak mata dapat menerjemahkan dalam piksel. Kemudian, posisi pointer pada sumbu x atau y diteruskan sebagai nilai. Untuk mendapatkan titik tengah mata saat menggerakkannya, mata diduplikasi. Objek asli tidak bergerak, transparan, dan digunakan sebagai referensi.

Kemudian, Anda perlu mengaitkannya dan memperbarui nilai properti kustom CSS pada mata agar mata dapat bergerak. Fungsi terikat ke peristiwa pointermove terhadap window. Saat ini diaktifkan, batas setiap mata 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 menarik di sini adalah menggunakan clamp() CSS untuk membuat perilaku yang 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);
 }

Mantra sihir

Jika Anda melihat halaman enam, apakah Anda merasa terpesona? Halaman ini menampilkan desain rubah ajaib kami yang fantastis. Jika Anda menggerakkan kursor, Anda mungkin akan 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 potret kita yang memproses peristiwa pointermove di window, elemen <canvas> kita juga demikian. Namun, setiap kali peristiwa diaktifkan, kita membuat objek untuk dianimasikan pada elemen <canvas>. Objek ini mewakili bentuk yang digunakan dalam jejak kursor. Mereka 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 diulang saat objek digambar ke elemen <canvas>. Properti untuk setiap objek memberi tahu elemen <canvas> tempat hal-hal 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. Kami 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 jejak. Setiap frame mengurangi ukuran dan mengubah posisi objek sebesar rate. Tindakan ini akan 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 beraksi.

Peninjauan aksesibilitas

Menciptakan pengalaman yang menyenangkan untuk dijelajahi memang bagus, tetapi tidak ada gunanya jika tidak dapat diakses oleh pengguna. Keahlian Adam di bidang ini terbukti sangat berharga dalam mempersiapkan Chrometober untuk peninjauan aksesibilitas sebelum dirilis.

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 segalanya lebih mudah diakses. Penggunaan judul dan link memudahkan pengguna untuk melakukan navigasi. Penggunaan daftar untuk halaman juga berarti jumlah halaman diumumkan oleh teknologi pendukung.
  • Memastikan semua gambar menggunakan atribut alt yang sesuai. Untuk SVG inline, elemen title ada jika diperlukan.
  • Menggunakan atribut aria jika dapat meningkatkan pengalaman. Penggunaan aria-label untuk halaman dan sisinya memberi tahu pengguna halaman mana yang sedang mereka buka. Penggunaan aria-describedBy pada link "Baca selengkapnya" menyampaikan teks blok konten. Hal ini menghilangkan ambiguitas tentang tujuan link tersebut.
  • Terkait blok konten, pengguna dapat mengklik seluruh kartu, bukan hanya link "Baca selengkapnya".
  • Penggunaan IntersectionObserver untuk melacak halaman mana yang sedang dilihat telah dibahas sebelumnya. Hal ini memiliki banyak manfaat yang tidak hanya terkait dengan performa. Halaman yang tidak terlihat akan menjeda animasi atau interaksi apa pun. Namun, halaman ini juga menerapkan atribut inert. 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 berpindah ke halaman lain menggunakan tombol tab.
  • Terakhir, kami menggunakan kueri media untuk menghormati preferensi pengguna terkait gerakan.

Berikut screenshot dari ulasan yang menyoroti beberapa tindakan yang diterapkan.

diidentifikasi sebagai elemen yang mencakup seluruh buku, yang menunjukkan bahwa elemen ini harus menjadi penanda utama yang dapat ditemukan oleh pengguna teknologi pendukung. More is outlined in the screenshot." width="800" height="465">

Screenshot buku Chrometober yang terbuka. Kotak bergaris tepi hijau disediakan di sekitar berbagai aspek UI, yang menjelaskan fungsi aksesibilitas yang diinginkan dan hasil pengalaman pengguna yang akan diberikan halaman. Misalnya, gambar memiliki teks alternatif. Contoh lainnya adalah label aksesibilitas yang menyatakan bahwa halaman yang tidak terlihat bersifat inert. More diuraikan dalam screenshot.

Yang kami pelajari

Motivasi di balik Chrometober tidak hanya untuk menyoroti konten web dari komunitas, tetapi juga merupakan cara bagi kami untuk menguji coba polyfill API animasi yang ditautkan ke scroll yang sedang dalam pengembangan.

Kami menyisihkan satu sesi saat menghadiri pertemuan tim di New York untuk menguji project dan mengatasi masalah yang muncul. Kontribusi tim sangat berharga. Ini juga merupakan kesempatan bagus untuk mencantumkan semua hal yang perlu ditangani sebelum kami dapat melakukan peluncuran.

Tim CSS, UI, dan DevTools duduk mengelilingi meja di ruang konferensi. Una berdiri di depan papan tulis yang dipenuhi sticky note. Anggota tim lainnya duduk mengelilingi meja dengan camilan dan laptop.

Misalnya, menguji buku di perangkat menimbulkan masalah rendering. Buku kami tidak akan dirender seperti yang diharapkan di perangkat iOS. Unit area tampilan 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 memunculkan beberapa masalah terkait polyfill API. Bramus mengangkat masalah ini di repositori polyfill. Dia kemudian menemukan solusi untuk masalah tersebut dan menggabungkannya ke dalam polyfill. Misalnya, permintaan pull ini meningkatkan performa dengan menambahkan penyimpanan ke dalam cache ke bagian polyfill.

Screenshot demo yang dibuka di Chrome. Alat Developer terbuka dan menampilkan pengukuran performa dasar pengukuran.

Screenshot demo yang dibuka di Chrome. Alat Developer terbuka dan menampilkan pengukuran performa yang lebih baik.

Selesai!

Project ini sangat menyenangkan untuk dikerjakan, menghasilkan pengalaman scroll yang unik dan menyoroti konten luar biasa dari komunitas. Tidak hanya itu, hal ini juga sangat bagus untuk menguji polyfill, serta memberikan masukan kepada tim engineering untuk membantu meningkatkan kualitas polyfill.

Chrometober 2022 telah berakhir.

Semoga Anda menyukainya. Apa fitur favorit Anda? Tweet saya dan beri tahu kami.

Jhey memegang lembaran stiker karakter dari Chrometober.

Anda bahkan mungkin bisa mendapatkan beberapa stiker dari salah satu anggota tim jika Anda bertemu kami di suatu acara.