Membangun Chrometober!

Cara buku scroll menjadi hidup karena berbagi tips dan trik yang menyenangkan dan menakutkan.

Sebagai tindak lanjut dari Designcember, kami ingin membuat Chrometober untuk Anda tahun ini sebagai cara untuk menyoroti dan membagikan konten web dari komunitas dan tim Chrome. Designcember menunjukkan penggunaan Kueri Penampung, tetapi tahun ini kami menampilkan API animasi tertaut scroll CSS.

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

Ringkasan

Tujuan proyek ini adalah memberikan pengalaman unik yang menyoroti API animasi tertaut-scroll. Namun, meskipun terlihat aneh, pengalaman tersebut juga harus responsif dan mudah diakses. Proyek ini juga merupakan cara yang bagus untuk menguji polyfill API yang sedang dalam pengembangan aktif; itu, serta mencoba berbagai teknik dan alat dalam kombinasinya. Dan semuanya dengan tema Halloween yang meriah!

Struktur tim kita tampak seperti ini:

Menyusun konsep pengalaman scrollytell

Ide untuk Chrometober mulai mengalir di tim pertama kami di luar lokasi perusahaan pada Mei 2022. Kumpulan coretan membuat kami memikirkan cara yang dapat digunakan pengguna untuk menggulir beberapa bentuk {i>storyboard<i}. Terinspirasi oleh video game, kami mempertimbangkan pengalaman bergulir melalui adegan seperti makam dan rumah hantu.

Sebuah buku catatan terletak di atas meja dengan berbagai coretan dan coretan yang terkait dengan proyek.

Saya sangat senang mendapatkan kebebasan berkreasi untuk membawa project Google pertama saya ke arah yang tidak terduga. Ini adalah prototipe awal tentang bagaimana pengguna dapat menavigasi konten.

Saat pengguna menggulir ke samping, blok akan berputar dan diskalakan. Namun, saya memutuskan untuk beralih dari ide ini karena khawatir tentang bagaimana kami bisa membuat pengalaman ini hebat bagi pengguna di semua ukuran perangkat. Alih-alih, saya menggunakan desain desain yang saya buat sebelumnya. Pada tahun 2020, saya beruntung memiliki akses ke ScrollTrigger dari GreenSock untuk membuat demo rilis.

Salah satu demo yang saya buat adalah buku 3D-CSS dengan halaman-halamannya berputar saat Anda men-scroll, dan ini terasa jauh lebih sesuai dengan apa yang kami inginkan untuk Chrometober. API animasi dengan scroll-link adalah pertukaran yang sempurna untuk fungsi tersebut. Ini juga berfungsi baik dengan scroll-snap, seperti yang akan Anda lihat.

Ilustrator kami untuk project, Tyler Reed, sangat ahli dalam mengubah desain seiring perubahan ide kami. Tyler melakukan pekerjaan yang fantastis dengan mewujudkan semua ide kreatif yang ia miliki dan mewujudkannya. Kegiatan {i>brainstorming<i} itu sangat menyenangkan. Sebagian besar cara kami ingin cara kerja ini adalah membuat fitur-fitur yang dipecah menjadi blok-blok yang terisolasi. Dengan begitu, kita bisa menyusunnya menjadi beberapa adegan, lalu memilih dan menciptakannya.

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

Ide utamanya adalah, saat pengguna menelusuri buku, dia dapat mengakses banyak konten. Mereka juga dapat berinteraksi dengan tanda-tanda ajaib, termasuk objek tersembunyi yang telah kami tanamkan dalam pengalaman tersebut; misalnya, potret di rumah berhantu, yang matanya mengikuti pointer 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 muncul dan diterjemahkan di sepanjang sumbu x saat pengguna menggulir.

Memahami API

Sebelum kita bisa mulai bermain dengan masing-masing fitur dan objek tersembunyi, kami membutuhkan buku. Jadi, kami memutuskan untuk mengubahnya menjadi kesempatan untuk menguji rangkaian fitur untuk CSS scroll-linked animation API yang muncul. API animasi dengan scroll-link saat ini tidak didukung di browser apa pun. Namun, saat mengembangkan API, engineer di tim interaksi telah mengerjakan polyfill. Ini menyediakan cara untuk menguji bentuk API seiring perkembangannya. Itu berarti kita bisa menggunakan API ini sekarang, dan proyek menyenangkan seperti ini sering kali menjadi tempat yang tepat untuk mencoba fitur eksperimental, dan untuk memberikan masukan. Cari tahu apa yang kami pelajari dan masukan yang dapat kami berikan, nanti dalam artikel.

Pada level tinggi, Anda dapat menggunakan API ini untuk menautkan animasi untuk men-scroll. Penting untuk diperhatikan bahwa Anda tidak dapat memicu animasi saat men-scroll—hal ini dapat terjadi nanti. Animasi scroll-link juga terbagi dalam dua kategori utama:

  1. yang bereaksi terhadap posisi scroll.
  2. Elemen 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 mengacu pada block logis. Animasi akan ditautkan untuk men-scroll dengan properti animation-timeline. animation-delay dan animation-end-delay (pada saat penulisan) adalah cara kami menentukan fase.

Fase-fase ini menentukan titik-titik di mana animasi harus ditautkan sehubungan dengan posisi elemen dalam container scroll-nya. Dalam contoh kita, kita mengatakan memulai animasi saat elemen memasuki (enter 0%) penampung scroll. Dan selesaikan setelah tertutup 50% (cover 50%) dari penampung scroll.

Berikut ini demo kita yang sedang dijalankan:

Anda juga dapat menautkan animasi ke elemen yang bergerak di area pandang. Anda dapat melakukannya dengan menetapkan animation-timeline menjadi view-timeline elemen. Ini bagus untuk skenario seperti animasi daftar. Perilaku ini mirip dengan cara menganimasikan elemen setelah entri 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" meningkatkan skala saat memasuki area pandang, sehingga memicu rotasi "Indikator".

Apa yang saya temukan dari eksperimen ini adalah bahwa API ini berfungsi sangat baik dengan scroll-snap. Scroll-snap yang dikombinasikan dengan ViewTimeline akan sangat cocok untuk mengikat pembalikan halaman dalam sebuah buku.

Membuat prototipe mekanisme

Setelah beberapa percobaan, saya bisa mendapatkan prototipe buku yang berfungsi. Anda scroll secara horizontal untuk membalik halaman buku.

Dalam demo, Anda dapat melihat berbagai pemicu yang disorot dengan batas putus-putus.

Markup terlihat sedikit 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 menggulir, halaman-halaman buku akan berputar, tetapi 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 berupa kemampuan melakukan loop atas sekumpulan elemen dan menghasilkan ViewTimeline yang kita butuhkan, 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 akan menganimasikan halaman terkait pemicu menggunakan ViewTimeline tersebut. Yang menautkan animasi halaman untuk men-scroll. Untuk animasi, kita memutar elemen halaman pada sumbu y untuk membalik halaman. Kita juga menerjemahkan halaman itu sendiri pada sumbu z sehingga perilakunya seperti buku.

Penutup

Setelah mengetahui mekanisme buku tersebut, saya bisa fokus untuk menghidupkan ilustrasi Tyler.

Astro

Tim menggunakan Astro untuk Designcember pada tahun 2021 dan saya ingin menggunakannya lagi untuk Chrometober. Pengalaman developer yang dapat memecah sesuatu menjadi beberapa komponen sangat cocok untuk project ini.

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

Membuat buku

Penting bagi saya untuk membuat blok tersebut mudah dikelola. Saya juga ingin memudahkan anggota tim lainnya untuk berkontribusi.

Halaman pada tingkat tinggi ditentukan oleh array konfigurasi. Setiap objek halaman dalam array menentukan konten, tampilan latar, dan metadata lainnya untuk sebuah 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 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 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 membuat ulang linimasa. 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 ini 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 arah buku membuat pemisahan bagian-bagian menjadi jauh lebih mudah, dan setiap bagian isi buku adalah adegan yang diambil dari desain aslinya.

Ilustrasi penyebaran halaman dari buku yang menampilkan pohon apel di kuburan. Makam memiliki beberapa nisan dan ada kelelawar di langit di depan bulan besar.

Karena kami telah memutuskan rasio aspek buku, tampilan latar untuk setiap halaman dapat memiliki elemen gambar. Menetapkan elemen tersebut ke lebar 200% dan menggunakan object-position berdasarkan sisi halaman akan memecahkannya.

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

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

Blok konten adalah link ke konten yang terlihat dalam buku. Objek 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 jika pemblokiran konten diperlukan. Kemudian, konfigurasi blok yang relevan diteruskan ke komponen ContentBlock.

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

Contoh di sini juga menunjukkan 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 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%;
}

Burung hantu adalah fitur interaktif—salah satu dari banyak fitur lain di project ini. Ini adalah contoh kecil yang bagus untuk dipelajari yang menunjukkan cara kami menggunakan Linimasa Tampilan bersama yang kami buat.

Pada level yang tinggi, komponen burung hantu mengimpor beberapa SVG dan menyisipkannya menggunakan Fragment Astro.

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

Dan gaya untuk memosisikan burung hantu kita ditempatkan bersama dengan kode komponen.

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

Ada satu bagian 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. Ini membuatnya relatif terhadap kotak pembatas objek dalam SVG. Burung hantu meningkatkan skala dari tengah bawah, sehingga digunakan transform-origin: 50% 100%.

Bagian yang menyenangkan adalah saat kita menautkan burung hantu 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:

  1. Periksa preferensi gerakan pengguna.
  2. Jika tidak memiliki preferensi, tautkan animasi burung hantu untuk men-scroll.

Untuk bagian kedua, burung hantu membuat animasi pada sumbu y menggunakan Web Animations API. Properti transformasi individual translate digunakan, dan ditautkan ke satu ViewTimeline. Akun ini ditautkan ke CHROMETOBER_TIMELINES[1] melalui properti timeline. Ini adalah ViewTimeline yang dibuat untuk pembalikan halaman. Ini akan menautkan animasi burung hantu ke membalik halaman menggunakan fase enter. Contoh ini menentukan bahwa, saat halaman diputar 80%, mulailah menggerakkan burung hantu. Pada level 90%, burung hantu itu 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 langsung terjun dan mengerjakan halaman atau fitur pilihan mereka. Berbagai fitur dalam buku memiliki animasi yang dikaitkan dengan membalik halaman buku; misalnya, pemukul yang terbang masuk dan keluar secara bergantian.

Kode ini juga memiliki elemen yang didukung oleh animasi CSS.

Setelah blok konten ada di dalam buku, ada waktu untuk berkreasi dengan fitur lainnya. Hal ini memberikan kesempatan untuk menghasilkan beberapa interaksi yang berbeda, dan mencoba berbagai cara untuk mengimplementasikan sesuatu.

Menjaga segala sesuatunya tetap responsif

Unit area pandang responsif menyesuaikan ukuran buku dan fiturnya. Namun, menjaga font tetap responsif adalah tantangan yang menarik. Unit kueri penampung cocok di sini. Meskipun demikian, API tersebut belum didukung di semua tempat. Ukuran buku sudah ditetapkan, jadi kita tidak memerlukan kueri container. Unit kueri penampung inline dapat dibuat dengan CSS calc() 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 memiliki pengamatan ini mungkin telah mengetahui penggunaan elemen <source> saat membahas tampilan latar halaman sebelumnya. Una sangat ingin melakukan interaksi dengan reaksi terhadap preferensi skema warna. Akibatnya, tampilan latar mendukung mode terang dan gelap dengan varian berbeda. Karena Anda dapat menggunakan kueri media dengan elemen <picture>, ini adalah cara yang tepat untuk menyediakan dua gaya tampilan latar. Elemen <source> membuat kueri untuk preferensi skema warna, dan menampilkan tampilan latar 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 bisa memasukkan perubahan lain berdasarkan preferensi skema warna tersebut. Labu di halaman kedua bereaksi terhadap preferensi skema warna pengguna. SVG yang digunakan memiliki lingkaran yang merepresentasikan api, yang akan meningkat skala dan bergerak 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 mengamati Anda?

Jika Anda memeriksa halaman 10, Anda mungkin menyadari sesuatu. Anda sedang ditonton! Mata potret akan mengikuti pointer 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, plus atau minus 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 sambil menggerakkannya, mata diduplikasi. Sumber asli tidak berpindah, transparan, dan digunakan sebagai referensi.

Lalu, kasus untuk menyatukannya dan memperbarui nilai properti kustom CSS di mata sehingga mata dapat bergerak. Fungsi terikat dengan peristiwa pointermove terhadap window. Saat dipicu, batas setiap mata akan digunakan untuk menghitung titik tengah. Kemudian, posisi pointer dipetakan ke nilai yang ditetapkan sebagai nilai properti kustom di 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 diinginkannya. Bagian hebatnya di sini adalah menggunakan CSS clamp() untuk membuat perilaku yang berbeda bagi setiap mata, sehingga Anda dapat membuat setiap mata berperilaku berbeda tanpa perlu 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);
 }

Mentransmisikan mantra

Jika Anda melihat halaman enam, apakah Anda merasa terpesona? Halaman ini berisi desain rubah ajaib yang fantastis. Jika menggerakkan kursor, Anda mungkin melihat efek jejak kursor kustom. Contoh ini menggunakan animasi kanvas. Elemen <canvas> berada di atas konten halaman lainnya dengan pointer-events: none. Ini berarti pengguna masih dapat mengeklik blok konten di bawahnya.

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

Sama seperti cara potret kita memproses peristiwa pointermove di window, begitu juga elemen <canvas>. Namun, setiap kali peristiwa dipicu, kita membuat objek untuk dianimasikan pada elemen <canvas>. Objek ini mewakili bentuk yang digunakan dalam jejak kursor. Mereka memiliki koordinat dan rona 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 sesuatu 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 akan dibuat dengan requestAnimationFrame. Jejak kursor hanya boleh dirender saat halaman terlihat. Kita memiliki IntersectionObserver yang memperbarui dan menentukan halaman yang ditampilkan. Jika sebuah halaman ditampilkan, objek akan dirender sebagai lingkaran di kanvas.

Kemudian, kita melakukan loop pada array blocks dan menggambar setiap bagian dari jalur tersebut. Setiap frame mengurangi ukuran dan mengubah posisi objek dengan rate. Tindakan ini menghasilkan efek penurunan 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 menjadi tidak terlihat, pemroses peristiwa akan dihapus dan loop frame animasi akan dibatalkan. Array blocks juga akan dihapus.

Inilah jejak kursor yang beraksi!

Peninjauan aksesibilitas

Menciptakan pengalaman yang menyenangkan untuk dijelajahi adalah hal yang bagus, tetapi tidak baik jika tidak dapat diakses oleh pengguna. Keahlian Adam dalam area ini terbukti sangat berharga dalam membuat Chrometober siap 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; selain penggunaan elemen <article> untuk setiap blok konten, dan elemen <abbr> tempat akronim diperkenalkan. Berpikir jauh ke depan ketika buku dibuat membuat segalanya 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, elemen title tersedia jika diperlukan.
  • Menggunakan atribut aria untuk meningkatkan pengalaman. Penggunaan aria-label untuk halaman dan sisinya memberi tahu pengguna di halaman mana halaman tersebut berada. Penggunaan aria-describedBy pada link "Baca selengkapnya" menyampaikan teks blok konten. Ini menghilangkan ambiguitas tentang ke mana tautan akan membawa pengguna.
  • Terkait pemblokiran konten, Anda dapat mengklik seluruh kartu dan bukan hanya link "Baca selengkapnya".
  • Penggunaan IntersectionObserver untuk melacak halaman mana yang terlihat yang ditampilkan sebelumnya. Hal ini memiliki banyak manfaat yang tidak hanya terkait dengan performa. Animasi atau interaksi di halaman yang tidak terlihat akan dijeda. 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 di dalam halaman yang terlihat dan pengguna tidak dapat membuka tab ke halaman lain.
  • Terakhir, kami menggunakan kueri media untuk menghormati preferensi pengguna terhadap gerakan.

Berikut screenshot dari ulasan yang menyoroti beberapa tindakan yang telah dilakukan.

diidentifikasi di seluruh bagian buku, yang menunjukkan bahwa elemen ini harus menjadi penanda utama untuk ditemukan oleh pengguna teknologi pendukung. Selengkapnya diuraikan di screenshot." width="800" height="465">

Screenshot buku Chrometober terbuka. Kotak bergaris tepi hijau disediakan di 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 di luar tampilan bersifat inert. Selengkapnya diuraikan dalam screenshot.

Yang kami pelajari

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

Kami menyisihkan sesi saat konferensi tim di New York untuk menguji proyek dan mengatasi masalah yang muncul. Kontribusi tim ini sangat berharga. Acara tersebut juga merupakan kesempatan bagus untuk membuat daftar semua hal yang perlu diatasi sebelum kami dapat melakukan live streaming.

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

Misalnya, pengujian buku di perangkat memunculkan masalah rendering. Buku kami tidak akan dirender seperti yang diharapkan pada perangkat iOS. Unit area pandang mengukur ukuran halaman, tetapi ketika 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 dengan polyfill API. Bramus melaporkan masalah ini di repositori polyfill. Dia kemudian menemukan solusi untuk masalah tersebut dan menggabungkan masalah tersebut ke dalam polyfill. Misalnya, permintaan pull ini menghasilkan peningkatan performa dengan menambahkan cache ke bagian polyfill.

Screenshot demo yang dibuka di Chrome. Developer Tools terbuka dan menunjukkan pengukuran performa dasar.

Screenshot demo yang dibuka di Chrome. Developer Tools terbuka dan menunjukkan pengukuran performa yang ditingkatkan.

Selesai.

Proyek ini benar-benar menyenangkan untuk dikerjakan, menghasilkan pengalaman scrolling unik yang menyoroti konten luar biasa dari komunitas. Bukan hanya itu, polyfill ini bagus juga untuk menguji polyfill, serta memberikan masukan kepada tim engineering untuk membantu memperbaiki polyfill.

Chrometober 2022 telah berakhir.

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

Jhey memegang lembar stiker karakter dari Chrometober.

Anda bahkan mungkin dapat mengambil beberapa stiker dari salah satu tim jika Anda melihat kami di sebuah acara.

Hero Photo oleh David Menidrey di Unsplash