Tworzę Chrometobera

Jak ta przewijająca się książka ożyła, aby podzielić się w tym roku w ramach Chrometobera przydatnymi i strasznymi poradami.

W nawiązaniu do projektu Designcember chcemy w tym roku stworzyć Chrometobera, w ramach którego będzie można wyróżniać i udostępniać treści internetowe przygotowane przez społeczność oraz zespół Chrome. W ubiegłym roku w ramach Designcember zaprezentowaliśmy zapytania do kontenera, ale w tym roku pokazujemy interfejs API animacji związanych z przewijaniem w CSS.

Zapoznaj się z przewijaną książką na stronie web.dev/chrometober-2022.

Omówienie

Celem projektu było dostarczenie zabawnego rozwiązania, które prezentuje interfejs API animacji związanych z przewijaniem. Choć interfejs był zabawny, jednocześnie musiał być łatwo dostępny i szybko reagować. Projekt był też świetnym sposobem na przetestowanie interfejsu API polyfill, który jest w trakcie opracowywania. Pozwalał też na wypróbowanie różnych technik i narzędzi w połączeniu. Wszystko w świątecznym klimacie Halloween.

Struktura naszego zespołu wyglądała tak:

Tworzenie wersji roboczej doświadczenia typu scrollytelling

Pomysły na temat Chrometober zaczęły napływać w maju 2022 r. podczas pierwszego spotkania zespołu poza biurem. Kolekcja rysunków skłoniła nas do zastanowienia się nad sposobami, w jakie użytkownik mógłby przewijać storyboard. Zainspirowani grami wideo, rozważaliśmy przewijanie obrazu przez takie sceny jak cmentarze czy nawiedzone domy.

Na biurku leży zeszyt z różnymi rysunkami i notatnikami związanymi z projektem.

Cieszył mnie fakt, że mogę wykorzystać moją kreatywność i podjąć się nieoczekiwanego projektu w Google. Był to wczesny prototyp pokazujący, jak użytkownik może poruszać się po treściach.

Gdy użytkownik przewija ekran w poziomie, bloki obracają się i powiększają. Postanowiłem jednak zrezygnować z tego pomysłu, ponieważ nie byłem pewien, jak zapewnić użytkownikom urządzeń o różnych rozmiarach jak najlepsze wrażenia. Zamiast tego zdecydowałem się na projekt, który stworzyłem wcześniej. W 2020 r. miałem szczęście mieć dostęp do ScrollTrigger firmy GreenSock, aby tworzyć wersje demonstracyjne.

Jedną z naszych wersji demonstracyjnych była książka w stylu 3D CSS, w której strony się przewracały w miarę ich przewijania, co sprawiało, że znacznie lepiej odpowiadały one Chrometoberowi. Interfejs API animacji powiązanych z przewijaniem jest idealnym zamiennikiem tej funkcji. Jak zobaczysz, dobrze współpracuje też z scroll-snap.

Nasz ilustrator, Tyler Reed, świetnie dostosowywał projekt do naszych zmieniających się pomysłów. Tyler świetnie wcielił w życie wszystkie pomysły, które mu rzucono. Wspólne burze mózgów było bardzo fajne. Ważnym elementem tego procesu było podzielenie funkcji na odrębne bloki. Dzięki temu mogliśmy stworzyć sceny, a potem wybrać i zastosować te, które najbardziej nam się spodobały.

Jedna ze scen składowych przedstawiająca węża, trumnę z wystającymi rękami, lisa z różdżką przy kotle, drzewo ze straszną twarzą i gargulca trzymającego lampion z dyni.

Głównym założeniem było to, że użytkownik mógł korzystać z bloków treści w miarę czytania książki. Mogą też wchodzić w interakcje z elementami odrobinę szalonymi, w tym z jajami wielkanocnymi, które wbudowaliśmy w tej grze. Na przykład portret w nawiedzonym domu, którego oczy podążają za kursorem, lub subtelne animacje wywoływane przez zapytania dotyczące multimediów. Te pomysły i funkcje byłyby animowane podczas przewijania. Na początku rozważaliśmy użycie królika zombie, który miałby się pojawiać i przesuwać wzdłuż osi X, gdy użytkownik przewija stronę.

Zapoznanie się z interfejsem API

Zanim zaczęliśmy bawić się poszczególnymi funkcjami i jajkami wielkanocnymi, potrzebowaliśmy książki. Dlatego postanowiliśmy wykorzystać tę okazję do przetestowania funkcji interfejsu CSS API animacji związanych z przewijaniem. Interfejs API animacji powiązanych z przewijaniem nie jest obecnie obsługiwany w żadnej przeglądarce. Podczas tworzenia interfejsu API inżynierowie z zespołu interakcji pracowali jednak nad polyfillem. Dzięki temu możesz testować interfejs API w trakcie jego tworzenia. Oznacza to, że możemy już teraz używać tego interfejsu API. Takie ciekawe projekty są często świetnym miejscem do testowania funkcji eksperymentalnych i przesyłania opinii. W dalszej części tego artykułu dowiesz się, czego się nauczyliśmy i jakiej udzieliliśmy odpowiedzi.

Ogólnie za pomocą tego interfejsu API możesz łączyć animacje na potrzeby przewijania. Pamiętaj, że nie można uruchamiać animacji podczas przewijania, ponieważ może to nastąpić później. Animacje połączone z przewijaniem dzielą się też na 2 główne kategorie:

  1. reagują na pozycję przewijania.
  2. Te, które reagują na pozycję elementu w jego przewijanym kontenerze.

Aby utworzyć tę drugą, używamy ViewTimeline zastosowanego za pomocą usługi animation-timeline.

Oto przykład użycia właściwości ViewTimeline w kodzie 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;
 }
}

Tworzymy element ViewTimeline o podanej wartości view-timeline-name i określamy dla niego oś. W tym przykładzie block odnosi się do logicznego block. Animacja zostaje powiązana z przewijaniem z usługą animation-timeline. animation-delayanimation-end-delay (w momencie pisania tego artykułu) to definicje faz.

Te etapy określają punkty, w których animacja powinna zostać połączona, względem pozycji elementu w jego przewijanym kontenerze. W naszym przykładzie animacja rozpoczyna się, gdy element wchodzi (enter 0%) do przewijanego kontenera. Zakończ, gdy obejmie ona 50% (cover 50%) kontenera przewijania.

Oto nasza prezentacja w akcji:

Możesz też połączyć animację z elementem, który porusza się w widoku. Możesz to zrobić, ustawiając animation-timeline jako view-timeline elementu. Jest to przydatne w przypadku animacji list. Działania te są podobne do tych, które wykonujesz, aby animować elementy po wejściu za pomocą 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;
  }
}

W tym przypadku „Mover” powiększa się, gdy wchodzi do widocznego obszaru, co powoduje obrót „Spinnera”.

Podczas eksperymentów okazało się, że interfejs API działa bardzo dobrze z przyspieszaniem przewijania. Przewijanie z przyspieszeniem w połączeniu z ViewTimeline świetnie sprawdzi się w przypadku przewracania stron w książce.

Prototypowanie mechaniki

Po kilku eksperymentach udało mi się uruchomić prototyp książki. Aby przewracać strony książki, przewijaj w poziomie.

W tym filmie uczymy się tworzyć reguły, a różne reguły są wyróżnione przerywanymi obrzeżeniami.

Oznakowanie wygląda mniej więcej tak:

<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>

Podczas przewijania strony książki zmieniają się, ale szybko się otwierają lub zamykają. Zależy to od wyrównania przesunięcia elementów sterujących.

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;
}

Tym razem nie łączymy ViewTimeline w CSS, ale używamy interfejsu Web Animations API w JavaScript. Dodatkową zaletą jest możliwość przetworzenia zestawu elementów i wygenerowania potrzebnych ViewTimeline zamiast tworzenia ich ręcznie.

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);

W przypadku każdego wyzwalacza generujemy ViewTimeline. Następnie za pomocą tego ViewTimeline animujemy stronę powiązaną z wyzwalaczem. Połączenie animacji strony z przewijaniem. W naszej animacji obracamy element strony wokół osi y, aby ją obrócić. Przesuwamy też samą stronę wzdłuż osi z, aby zachowywała się jak książka.

Podsumowanie

Gdy udało mi się opracować mechanizm książki, mogłam skupić się na stworzeniu ilustracji Tylera.

Astro

W 2021 r. zespół użył Astro w ramach Designcember, a ja chciałam ponownie wykorzystać tę czcionkę w Chrometoberze. W tym projekcie dobrze sprawdza się możliwość dzielenia elementów na komponenty.

Książka jest komponentem. Jest to także zbiór komponentów strony. Każda strona ma dwie strony i tło. Podrzędne strony to komponenty, które można łatwo dodawać, usuwać i umieszczać.

Tworzenie książki

Ważne było dla mnie, aby można było łatwo zarządzać blokami. Chciałem też ułatwić reszcie zespołu wnoszenie swoich uwag.

Strony na najwyższym poziomie są definiowane przez tablicę konfiguracji. Każdy obiekt strony w tablicy określa zawartość, tło i inne metadane strony.

const pages = [
  {
    front: {
      marked: true,
      content: PageTwo,
      backdrop: spreadOne,
      darkBackdrop: spreadOneDark
    },
    back: {
      content: PageThree,
      backdrop: spreadTwo,
      darkBackdrop: spreadTwoDark
    },
    aria: `page 1`
  },
  /* Obfuscated page objects */
]

Zostaną one przekazane do komponentu Book.

<Book pages={pages} />

W komponencie Book jest stosowany mechanizm przewijania i tworzy się w nim strony książki. Używamy tego samego mechanizmu co w prototypie, ale udostępniamy wiele instancji ViewTimeline, które są tworzone globalnie.

window.CHROMETOBER_TIMELINES.push(viewTimeline);

Dzięki temu możemy udostępniać osi czasu do wykorzystania w innych miejscach zamiast ich ponownego tworzenia. Więcej informacji na ten temat znajdziesz później.

Struktura strony

Każda strona jest elementem listy:

<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>

Zdefiniowana konfiguracja jest przekazywana do każdego wystąpienia Page. Strony korzystają z funkcji slotu w Astro, aby wstawiać treści na poszczególnych stronach.

<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>

Ten kod służy głównie do konfigurowania struktury. Większość treści książki mogą tworzyć współtwórcy bez konieczności modyfikowania tego kodu.

W tle

Przejście na kreację w postaci książki znacznie ułatwiło podział na sekcje, a każda strona książki to scena pochodząca z pierwotnego projektu.

Ilustracja przedstawiająca rozłożone strony z książki, na której widać jabłoń na cmentarzu. Cmentarz z wieloma nagrobkami. Na niebie widać nietoperza i księżyc.

Ponieważ zdecydowaliśmy się na format obrazu, tło każdej strony może zawierać element graficzny. Ustawienie szerokości tego elementu na 200% i użycie atrybutu object-position w zależności od strony powinno rozwiązać problem.

.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;
}

Zawartość strony

Spójrzmy na utworzenie jednej ze stron. Strona 3 zawiera sowę, która pojawia się na drzewie.

Jest on wypełniany komponentem PageThree zgodnie z definicją w konfiguracji. Jest to komponent Astro (PageThree.astro). Takie komponenty wyglądają jak pliki HTML, ale na górze mają element kodu podobny do frontmatter. Dzięki temu możemy na przykład importować inne komponenty. Komponent na stronie 3 wygląda tak:

---
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>

Ponownie, strony są atomowe. Składają się one z zestawu funkcji. Strona 3 zawiera blok treści i interaktywną sowę, więc dla każdego z nich jest komponent.

Bloki treści to linki do treści wyświetlanych w książce. Te dane są też określane przez obiekt konfiguracji.

{
 "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
  ]
}

Ta konfiguracja jest importowana w przypadku bloków treści. Następnie odpowiednia konfiguracja bloku jest przekazywana do komponentu ContentBlock.

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

Poniżej znajdziesz też przykład użycia komponentu strony jako miejsca na treści. Tutaj umieszczany jest blok treści.

<style is:global>
  .content-block--four {
    left: 30%;
    bottom: 10%;
  }
</style>

Ogólne style bloku treści są jednak zlokalizowane razem z kodem komponentu.

.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%;
}

Sowa to interaktywna funkcja – jedna z wielu w tym projekcie. Ten krótki przykład pokazuje, jak wykorzystaliśmy utworzony przez nas udostępniony widok Oś czasu.

Ogólnie rzecz biorąc, nasz komponent sowy importuje plik SVG i wstawia go w postaci fragmentu za pomocą funkcji Fragment w Astro.

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

Styli pozycjonowania sowy znajdują się razem z kodem komponentu.

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

Jest jeden dodatkowy element stylizacji, który określa zachowanie transform sowy.

.owl__owl {
  transform-origin: 50% 100%;
  transform-box: fill-box;
}

Korzystanie z funkcji transform-box wpływa na transform-origin. Jest ona względna względem prostokąta ograniczającego obiekt w pliku SVG. Sowa powiększa się od dołu środka, dlatego używamy transform-origin: 50% 100%.

Najciekawsza część to ta, w której sowa łączy się z jednym z wygenerowanych ViewTimeline:

const setUpOwl = () => {
   const owl = document.querySelector('.owl__owl');

   owl.animate([
     {
       translate: '0% 110%',
     },
     {
       translate: '0% 10%',
     },
   ], {
     timeline: CHROMETOBER_TIMELINES[1],
     delay: { phase: "enter", percent: CSS.percent(80) },
     endDelay: { phase: "enter", percent: CSS.percent(90) },
     fill: 'both' 
   });
 }

 if (window.matchMedia('(prefers-reduced-motion: no-preference)').matches)
   setUpOwl()

W tym bloku kodu wykonujemy 2 działania:

  1. Sprawdź ustawienia użytkownika dotyczące ruchu.
  2. Jeśli ta osoba nie ma w tym nic wspólnego, dodaj do niej link z animacją przedstawiającą sową, aby ją przewijać.

W drugiej części sowa porusza się wzdłuż osi Y za pomocą interfejsu Web Animations API. Używana jest indywidualna właściwość transformacji translate, która jest powiązana z jednym elementem ViewTimeline. Jest ona połączona z usługą CHROMETOBER_TIMELINES[1] za pomocą usługi timeline. Jest to ViewTimeline generowany w przypadku przewracania stron. W ten sposób animacja sowy zostanie powiązana z przewracaniem strony za pomocą fazy enter. Określa, że gdy strona jest przewinięta w 80%, sowa zaczyna się poruszać. Na 90% sowa powinna skończyć tłumaczenie.

Funkcje książki

Poznaliśmy już metodę tworzenia strony i architekturę projektu. Możesz zobaczyć, jak pozwala ona współtwórcom przejść do pracy nad wybraną przez nich stroną lub funkcją. Animacje różnych elementów książki są powiązane z przewracaniem stron, np. nietoperz, który wlatuje i zlatuje przy przewracaniu kart.

Zawiera też elementy z animacjami CSS.

Gdy bloki treści znalazły się w książce, nadszedł czas na kreatywne wykorzystanie innych funkcji. Dzięki temu mogliśmy wygenerować różne interakcje i wypróbować różne sposoby implementacji.

Zapewnienie responsywności

Elastyczne widoczne obszary określają rozmiar książki i jej funkcji. Jednak utrzymanie responsywności czcionek było ciekawym wyzwaniem. W takim przypadku dobrze sprawdzą się jednostki zapytania dotyczące kontenera. Nie są one jednak obsługiwane wszędzie. Rozmiar książki jest ustawiony, więc nie potrzebujemy zapytania dotyczącego kontenera. Jednostka zapytania wbudowanego kontenera może być generowana za pomocą CSS calc() i używana do określania rozmiaru czcionki.


.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);
}

Dynie świecące w nocy

Bycie słabszym może już zauważyć użycie elementów <source> podczas omawiania tła strony. Una chciała nawiązać interakcję, która zareagowała na preferowany schemat kolorów. W efekcie tła obsługują zarówno tryb jasny, jak i ciemny z różnymi wariantami. Element <picture> umożliwia korzystanie z zapytań dotyczących multimediów, dzięki czemu możesz podać 2 style tła. Element <source> wysyła zapytanie o preferowany schemat kolorów i wyświetla odpowiednie tło.

<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>

Możesz wprowadzić inne zmiany na podstawie preferowanego schematu kolorów. Dynie na stronie 2 reagują na preferowany schemat kolorów użytkownika. Użyty plik SVG zawiera koła przedstawiające płomienie, które powiększają się i animują w ciemnym trybie.

.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;
     }
   }
 }

Czy ten portret Cię obserwuje?

Jeśli sprawdzisz stronę 10, możesz coś zauważyć. Jesteś obserwowany! Gdy będziesz poruszać kursorem po stronie, oczy portretu będą podążać za nim. Sztuczka polega na zmapowaniu lokalizacji wskaźnika na wartość tłumaczenia i przekazaniu jej do 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)
 }

Ten kod przyjmuje zakresy wejściowe i wyjściowe oraz mapuje podane wartości. W tym przykładzie wartość wynosi 625.

mapRange(0, 100, 250, 1000, 50) // 625

W przypadku portretu wartością wejściową jest punkt środkowy każdego oka z dodanym lub odjętym pewną liczbą pikseli. Zakres wyjściowy to liczba pikseli, na które oczy mogą przetłumaczyć obraz. Następnie jako wartość przekazywana jest pozycja wskaźnika na osi X lub Y. Aby uzyskać punkt środkowy oczu podczas ich przemieszczania, oczy są powielane. Oryginały nie przesuwają się, są przezroczyste i służą jako odniesienia.

Następnie trzeba połączyć je ze sobą i zaktualizować wartości właściwości niestandardowych CSS w oczach, aby mogły się ruszać. Funkcja jest powiązana z wydarzeniem pointermove w przypadku window. Gdy to zdarzenie zostanie wywołane, do obliczenia punktów środkowych zostaną użyte granice każdego oka. Następnie pozycja wskaźnika jest mapowana na wartości ustawione jako wartości właściwości niestandardowych oczu.

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)
     })
 }

Gdy wartości zostaną przekazane do CSS, style mogą z nimi robić, co tylko chcą. Najważniejsze jest tu użycie CSS clamp(), aby zmienić zachowanie każdego oka. Dzięki temu możesz zmienić zachowanie każdego oka bez konieczności ponownego korzystania z JavaScriptu.

.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);
 }

rzucanie zaklęć;

Czy po szóstej stronie czujesz się w klimacie? Ta strona wykorzystuje projekt naszej fantastycznej magicznej lisicy. Jeśli przesuniesz kursor, możesz zobaczyć niestandardowy efekt ścieżki kursora. Ta animacja wykorzystuje animację na płótnie. Element <canvas> znajduje się nad resztą treści strony z atrybutem pointer-events: none. Oznacza to, że użytkownicy nadal mogą klikać bloki treści znajdujące się pod reklamą.

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

Podobnie jak w przypadku elementu pointermove, element <canvas> reaguje na zdarzenie pointermove w komponencie window. Jednak po każdym uruchomieniu zdarzenia tworzymy obiekt animowany w elemencie <canvas>. Te obiekty reprezentują kształty używane w śladzie kursora. Mają współrzędne i losowy odcień.

Funkcja mapRange jest używana ponownie, ponieważ możemy jej użyć do zmapowania delty wskaźnika na sizerate. Obiekty są przechowywane w tablicy, która jest iterowana, gdy obiekty są rysowane w elemencie <canvas>. Właściwości każdego obiektu określają, gdzie element <canvas> ma narysować swoje elementy.

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)

Podczas rysowania w obszarze roboczym tworzona jest pętla za pomocą funkcji requestAnimationFrame. Ślad kursora powinien być renderowany tylko wtedy, gdy strona jest widoczna. Mamy IntersectionObserver, który aktualizuje i określa, które strony są widoczne. Jeśli strona jest widoczna, obiekty są renderowane w obszarze roboczym w postaci okręgów.

Następnie wykonujemy pętlę po tablicy blocks i rysujemy każdą część ścieżki. Każda klatka zmniejsza rozmiar i zmienia pozycję obiektu o rate. W ten sposób powstaje efekt opadania i skalowania. Jeśli obiekt całkowicie się skurczy, zostanie usunięty z tablicy 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)
 }

Jeśli strona zniknie z pola widzenia, detektory zdarzeń zostaną usunięte, a pętla ramki animacji zostanie anulowana. Tablica blocks również jest wyczyszczona.

Oto działanie śladu kursora.

Weryfikacja ułatwień dostępu

Fajnie jest tworzyć ciekawe treści, ale nie ma sensu, jeśli nie są one dostępne dla użytkowników. Adam ma ogromną wiedzę w tej dziedzinie, co okazało się bezcenne w przygotowaniu Chrometobera do sprawdzenia ułatwień dostępu przed wydaniem.

Oto niektóre z nich:

  • Upewnij się, że użyty kod HTML jest semantyczny. Dotyczyło to takich elementów jak <main> w książce, element <article> w każdym bloku treści oraz elementy <abbr>, w których pojawiają się akronimy. Przewidywanie przyszłości podczas tworzenia książki ułatwiło mi tworzenie treści. Użycie nagłówków i linków ułatwia użytkownikowi poruszanie się po stronie. Użycie listy stron oznacza też, że liczba stron jest ogłaszana przez technologię ułatwień dostępu.
  • Upewnij się, że wszystkie obrazy używają odpowiednich atrybutów alt. W przypadku wstawianych plików SVG element title jest obecny w miejscach, w których jest to konieczne.
  • Używanie atrybutów aria, które polepszają działanie aplikacji. Użycie aria-label dla stron i ich stron bocznych informuje użytkownika, na której stronie się znajduje. Użycie aria-describedBy w linkach „Pokaż więcej” informuje o treści bloku treści. Dzięki temu użytkownik nie będzie miał wątpliwości, dokąd prowadzi link.
  • W przypadku blokad treści można kliknąć całą kartę, a nie tylko link „Czytaj więcej”.
  • Korzystanie z elementu IntersectionObserver do śledzenia, które strony są widoczne, zostało omówione wcześniej. Ma to wiele zalet, które nie są związane tylko z wydajnością. W przypadku stron, które nie są widoczne, animacje lub interakcje zostaną wstrzymane. Te strony mają też zastosowany atrybut inert. Oznacza to, że użytkownicy korzystający z czytnika ekranu mogą przeglądać te same treści co użytkownicy widzący. Punkt skupienia pozostaje na stronie, która jest widoczna, a użytkownicy nie mogą przełączyć się na inną stronę.
  • Na koniec warto wspomnieć, że korzystamy z zapytań dotyczących multimediów, aby uwzględnić preferencje użytkownika dotyczące animacji.

Oto zrzut ekranu z sprawdzaniem, który pokazuje niektóre zastosowane środki.

element jest zidentyfikowany jako obejmujący całą książkę, co oznacza, że powinien być głównym punktem orientacyjnym dla użytkowników technologii wspomagających. Więcej informacji znajdziesz na zrzucie ekranu." width="800" height="465">

Zrzut ekranu otwartej książki z okazji Chrometober. Wokół różnych aspektów interfejsu zaznaczono zielone pola, które opisują zamierzone ułatwienia dostępu i jakie będą wrażenia użytkownika. Na przykład obrazy mają tekst alternatywny. Innym przykładem jest etykieta ułatwień dostępu z informacją, że strony nie są wyświetlane jako bezwładne. Więcej informacji znajdziesz na zrzucie ekranu.

Czego się nauczyliśmy?

Motywacją do zorganizowania Chrometobera było nie tylko wyróżnienie treści internetowych od społeczności, ale też przetestowanie interfejsu API polyfill do animacji związanych z przewijaniem, który jest obecnie w fazie rozwoju.

Podczas szczytu naszego zespołu w Nowym Jorku poświęciliśmy sesję na przetestowanie projektu i rozwiązanie powstałych problemów. Ich wkład był nieoceniony. Była to też świetna okazja, aby wymienić wszystkie kwestie, które trzeba było rozwiązać, zanim mogliśmy wdrożyć usługę.

Zespoły CSS, UI i Narzędzia deweloperskie siedzą przy stole w sali konferencyjnej. Una stoi przy tablicy pokrytej notatkami samoprzylepnymi. Pozostali członkowie zespołu siedzą przy stole, z przekąskami i laptopami.

Na przykład testowanie książki na urządzeniach spowodowało problem z renderowaniem. Na urządzeniach z iOS nasza książka nie była renderowana zgodnie z oczekiwaniami. Jednostki widocznego obszaru określają rozmiar strony, ale gdy notch był obecny, wpływało to na książkę. Rozwiązaniem było użycie viewport-fit=cover w widocznym obszarze meta:

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />

W ramach tej sesji poruszono też kilka problemów z polyfillem interfejsu API. Usługa Bramus zgłosiła te problemy w repozytorium polyfill. Następnie znalazł rozwiązania tych problemów i złączył je z polyfill. Na przykład ta prośba o wybieranie zwiększyła wydajność dzięki dodaniu do części polyfill pamięci podręcznej.

Zrzut ekranu pokazujący demonstrację w Chrome. Narzędzia dla programistów są otwarte i wyświetlają podstawowe pomiary wydajności.

Zrzut ekranu pokazujący demonstrację w Chrome Narzędzia dla programistów są otwarte i wyświetlają ulepszony pomiar wydajności.

Znakomicie.

Praca nad tym projektem była naprawdę przyjemna, a przewijanie gry było zabawne, a w efekcie wyróżniane są wyjątkowe treści opublikowane przez społeczność. Testowanie kodu polyfill okazało się bardzo przydatne, a dodatkowo zespół inżynierski otrzymał opinie, które pomogą go ulepszyć.

Chrometober 2022 okazał się sukcesem.

Mamy nadzieję, że Ci się podobało. Jaka jest Twoja ulubiona funkcja? Tweetuj do mnie i daj nam znać.

Jhey trzyma naklejki z postaciami z Chrometober.

Jeśli spotkasz nas na wydarzeniu, możesz nawet otrzymać kilka naklejek od członka zespołu.

Zdjęcie główne: David Menidrey, Unsplash