Строим Хромтобер!

Как появилась на свет книга с прокруткой, благодаря которой этот Хромтобер поделился забавными и пугающими советами и приемами.

Вслед за Designcember в этом году мы хотели создать для вас Chrometober, чтобы можно было выделить и поделиться веб-контентом от сообщества и команды Chrome. Designcember продемонстрировал использование контейнерных запросов, но в этом году мы демонстрируем API анимации с прокруткой CSS.

Ознакомьтесь с возможностями прокрутки книг на странице web.dev/chrometober-2022 .

Обзор

Целью проекта было предоставить причудливый опыт, подчеркивающий API анимации, связанной с прокруткой. Но, несмотря на свою причудливость, этот опыт также должен был быть отзывчивым и доступным. Этот проект также стал отличным способом протестировать полифил API, который находится в активной разработке; это, а также сочетание различных техник и инструментов. И все это в праздничной теме Хэллоуина!

Структура нашей команды выглядела так:

Разработка опыта прокрутки

Идеи для Chrometober начали поступать в нашу первую выездную команду еще в мае 2022 года. Коллекция каракулей заставила нас задуматься о том, как пользователь мог бы прокручивать свой путь по той или иной форме раскадровки. Вдохновленные видеоиграми, мы рассмотрели возможность прокрутки таких сцен, как кладбища и дом с привидениями.

На столе лежит блокнот с различными рисунками и каракулями, связанными с проектом.

Было здорово иметь творческую свободу и направить мой первый проект Google в неожиданном направлении. Это был ранний прототип того, как пользователь может перемещаться по контенту.

Когда пользователь прокручивает страницу вбок, блоки вращаются и масштабируются. Но я решил отойти от этой идеи, беспокоясь о том, как мы можем сделать этот опыт удобным для пользователей на устройствах всех размеров. Вместо этого я склонялся к дизайну того, что делал раньше. В 2020 году мне посчастливилось получить доступ к ScrollTrigger от GreenSock для создания демо-версий.

Одной из созданных мною демонстраций была книга на 3D-CSS, в которой страницы переворачивались при прокрутке, и это казалось гораздо более подходящим для того, что мы хотели для Chrometober. API анимации, связанной с прокруткой, — идеальная замена этой функциональности. Как вы увидите, он также хорошо работает с scroll-snap !

Наш иллюстратор проекта, Тайлер Рид , отлично умел менять дизайн по мере того, как мы меняли идеи. Тайлер проделал фантастическую работу, вобрав в себя все творческие идеи, брошенные ему в голову, и воплотив их в жизнь. Было очень весело вместе обсуждать идеи. Большая часть того, как мы хотели, чтобы это работало, заключалась в том, чтобы функции были разбиты на отдельные блоки. Таким образом, мы могли скомпоновать их в сцены, а затем выбирать, что воплотить в жизнь.

В одной из сцен композиции изображены змея, гроб с вытянутыми руками, лиса с палочкой у котла, дерево с жуткой мордой и горгулья, держащая тыквенный фонарь.

Основная идея заключалась в том, что, просматривая книгу, пользователь мог получить доступ к блокам контента. Они также могли взаимодействовать с капризами, в том числе с пасхальными яйцами, которые мы встроили в этот опыт; например, портрет в доме с привидениями, чьи глаза следили за вашим указателем, или тонкая анимация, запускаемая медиа-запросами. Эти идеи и функции будут анимированы при прокрутке. Первой идеей был кролик-зомби, который поднимался и перемещался вдоль оси X при прокрутке пользователем.

Знакомство с API

Прежде чем мы могли начать играть с отдельными чертами и пасхалками, нам понадобилась книга. Поэтому мы решили использовать это как шанс протестировать набор функций для нового CSS-API анимации с прокруткой . API анимации, связанной с прокруткой, в настоящее время не поддерживается ни в одном браузере. Однако при разработке API инженеры группы взаимодействия работали над полифиллом . Это дает возможность протестировать форму API по мере его разработки. Это означает, что мы могли бы использовать этот API уже сегодня, и подобные забавные проекты часто являются отличным местом для опробования экспериментальных функций и обратной связи. Узнайте, что мы узнали и какие отзывы смогли предоставить, далее в статье .

На высоком уровне вы можете использовать этот API для привязки анимации к прокрутке. Важно отметить, что вы не можете активировать анимацию при прокрутке — это может появиться позже. Анимации, связанные с прокруткой, также делятся на две основные категории:

  1. Те, которые реагируют на положение прокрутки.
  2. Те, которые реагируют на положение элемента в его контейнере прокрутки.

Для создания последнего мы используем ViewTimeline , применяемый через свойство animation-timeline .

Вот пример того, как выглядит использование ViewTimeline в CSS:

.element-moving-in-viewport {
  view-timeline-name: foo;
  view-timeline-axis: block;
}

.element-scroll-linked {
  animation: rotate both linear;
  animation-timeline: foo;
  animation-delay: enter 0%;
  animation-end-delay: cover 50%;
}

@keyframes rotate {
 to {
   rotate: 360deg;
 }
}

Мы создаем ViewTimeline с view-timeline-name и определяем для него ось. В этом примере block относится к логическому block . Анимация привязывается к прокрутке с помощью animation-timeline . animation-delay и animation-end-delay (на момент написания) — это то, как мы определяем фазы.

Эти фазы определяют точки, в которых анимация должна быть связана с позицией элемента в его контейнере прокрутки. В нашем примере мы говорим, что анимация начинается, когда элемент входит ( enter 0% ) в контейнер прокрутки. И закончите, когда он покроет 50% ( cover 50% ) контейнера прокрутки.

Вот наша демонстрация в действии:

Вы также можете связать анимацию с элементом, который перемещается в области просмотра. Вы можете сделать это, установив animation-timeline view-timeline элемента. Это хорошо для таких сценариев, как анимация списков. Поведение аналогично тому, как вы можете анимировать элементы при вводе с помощью IntersectionObserver .

element-moving-in-viewport {
  view-timeline-name: foo;
  view-timeline-axis: block;
  animation: scale both linear;
  animation-delay: enter 0%;
  animation-end-delay: cover 50%;
  animation-timeline: foo;
}

@keyframes scale {
  0% {
    scale: 0;
  }
}

При этом «Двигатель» увеличивается при входе в область просмотра, вызывая вращение «Спиннера».

В ходе экспериментов я обнаружил, что API очень хорошо работает с прокруткой . Привязка прокрутки в сочетании с ViewTimeline отлично подойдет для привязки перелистывания страниц в книге.

Прототипирование механики

После некоторых экспериментов мне удалось заставить работать прототип книги. Вы прокручиваете страницы по горизонтали, чтобы перелистывать страницы книги.

В демо-версии вы можете увидеть различные триггеры, выделенные пунктирными рамками.

Разметка выглядит примерно так:

<body>
  <div class="book-placeholder">
    <ul class="book" style="--count: 7;">
      <li
        class="page page--cover page--cover-front"
        data-scroll-target="1"
        style="--index: 0;"
      >
        <div class="page__paper">
          <div class="page__side page__side--front"></div>
          <div class="page__side page__side--back"></div>
        </div>
      </li>
      <!-- Markup for other pages here -->
    </ul>
  </div>
  <div>
    <p>intro spacer</p>
  </div>
  <div data-scroll-intro>
    <p>scale trigger</p>
  </div>
  <div data-scroll-trigger="1">
    <p>page trigger</p>
  </div>
  <!-- Markup for other triggers here -->
</body>

При прокрутке страницы книги переворачиваются, но при этом открываются или закрываются. Это зависит от выравнивания триггеров при прокрутке.

html {
  scroll-snap-type: x mandatory;
}

body {
  grid-template-columns: repeat(var(--trigger-count), auto);
  overflow-y: hidden;
  overflow-x: scroll;
  display: grid;
}

body > [data-scroll-trigger] {
  height: 100vh;
  width: clamp(10rem, 10vw, 300px);
}

body > [data-scroll-trigger] {
  scroll-snap-align: end;
}

На этот раз мы не подключаем ViewTimeline в CSS, а используем API веб-анимации в JavaScript. Это дает дополнительное преимущество, заключающееся в возможности перебирать набор элементов и генерировать нужную нам ViewTimeline вместо того, чтобы создавать их каждый вручную.

const triggers = document.querySelectorAll("[data-scroll-trigger]")

const commonProps = {
  delay: { phase: "enter", percent: CSS.percent(0) },
  endDelay: { phase: "enter", percent: CSS.percent(100) },
  fill: "both"
}

const setupPage = (trigger, index) => {
  const target = document.querySelector(
    `[data-scroll-target="${trigger.getAttribute("data-scroll-trigger")}"]`
  );

  const viewTimeline = new ViewTimeline({
    subject: trigger,
    axis: 'inline',
  });

  target.animate(
    [
      {
        transform: `translateZ(${(triggers.length - index) * 2}px)`
      },
      {
        transform: `translateZ(${(triggers.length - index) * 2}px)`,
        offset: 0.75
      },
      {
        transform: `translateZ(${(triggers.length - index) * -1}px)`
      }
    ],
    {
      timeline: viewTimeline,
      …commonProps,
    }
  );
  target.querySelector(".page__paper").animate(
    [
      {
        transform: "rotateY(0deg)"
      },
      {
        transform: "rotateY(-180deg)"
      }
    ],
    {
      timeline: viewTimeline,
      …commonProps,
    }
  );
};

const triggers = document.querySelectorAll('[data-scroll-trigger]')
triggers.forEach(setupPage);

Для каждого триггера мы генерируем ViewTimeline . Затем мы анимируем связанную с триггером страницу, используя ViewTimeline . Это связывает анимацию страницы с прокруткой. В нашей анимации мы поворачиваем элемент страницы по оси Y, чтобы перевернуть страницу. Мы также переводим саму страницу по оси Z, чтобы она вела себя как книга.

Собираем все это вместе

Разработав механизм книги, я смог сосредоточиться на воплощении в жизнь иллюстраций Тайлера.

Астро

Команда использовала Astro для Designcember в 2021 году, и мне очень хотелось использовать его снова для Chrometober. Опыт разработчиков, позволяющий разбивать все на компоненты, хорошо подходит для этого проекта.

Сама книга является компонентом. Это также коллекция компонентов страницы. Каждая страница имеет две стороны и фон. Дочерние части страницы — это компоненты, которые можно легко добавлять, удалять и размещать.

Создание книги

Для меня было важно сделать блоки удобными в управлении. Я также хотел, чтобы остальной команде было легко вносить свой вклад.

Страницы на высоком уровне определяются массивом конфигурации. Каждый объект страницы в массиве определяет содержимое, фон и другие метаданные страницы.

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

Они передаются компоненту Book .

<Book pages={pages} />

В компоненте Book применяется механизм прокрутки и создаются страницы книги. Используется тот же механизм, что и в прототипе; но мы используем несколько экземпляров ViewTimeline , созданных глобально.

window.CHROMETOBER_TIMELINES.push(viewTimeline);

Таким образом, мы можем поделиться временными рамками для использования в другом месте, а не воссоздавать их. Подробнее об этом позже.

Состав страницы

Каждая страница представляет собой элемент списка внутри списка:

<ul class="book">
  {
    pages.map((page, index) => {
      const FrontSlot = page.front.content
      const BackSlot = page.back.content
      return (
        <Page
          index={index}
          cover={page.cover}
          aria={page.aria}
          backdrop={
            {
              front: {
                light: page.front.backdrop,
                dark: page.front.darkBackdrop
              },
              back: {
                light: page.back.backdrop,
                dark: page.back.darkBackdrop
              }
            }
          }>
          {page.front.content && <FrontSlot slot="front" />}    
          {page.back.content && <BackSlot slot="back" />}    
        </Page>
      )
    })
  }
</ul>

И определенная конфигурация передается каждому экземпляру Page . Страницы используют функцию слота Astro для вставки контента на каждую страницу.

<li
  class={className}
  data-scroll-target={target}
  style={`--index:${index};`}
  aria-label={aria}
>
  <div class="page__paper">
    <div
      class="page__side page__side--front"
      aria-label={`Right page of ${index}`}
    >
      <picture>
        <source
          srcset={darkFront}
          media="(prefers-color-scheme: dark)"
          height="214"
          width="150"
        >
        <img
          src={lightFront}
          class="page__background page__background--right"
          alt=""
          aria-hidden="true"
          height="214"
          width="150"
        >
      </picture>
      <div class="page__content">
        <slot name="front" />
      </div>
    </div>
    <!-- Markup for back page -->
  </div>
</li>

Этот код в основном предназначен для настройки структуры. Авторы могут работать над содержимым книги по большей части, не прикасаясь к этому коду.

Фоны

Творческий сдвиг в сторону книги значительно облегчил разделение разделов, и каждый разворот книги представляет собой сцену, взятую из оригинального дизайна.

Страница разворачивается с иллюстрацией из книги, на которой изображена яблоня на кладбище. На кладбище есть несколько надгробий, а в небе перед большой луной виднеется летучая мышь.

Поскольку мы определились с соотношением сторон книги, фон каждой страницы может иметь элемент изображения. Установка ширины этого элемента на 200% и использование object-position в зависимости от стороны страницы помогут.

.page__background {
  height: 100%;
  width: 200%;
  object-fit: cover;
  object-position: 0 0;
  position: absolute;
  top: 0;
  left: 0;
}

.page__background--right {
  object-position: 100% 0;
}

Содержимое страницы

Давайте посмотрим на создание одной из страниц. На третьей странице изображена сова, которая появляется на дереве.

Он заполняется компонентом PageThree , как определено в конфигурации. Это компонент Astro ( PageThree.astro ). Эти компоненты выглядят как файлы HTML, но у них есть граница кода вверху, похожая на фронтальную часть. Это позволяет нам делать такие вещи, как импорт других компонентов. Компонент третьей страницы выглядит следующим образом:

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

Опять же, страницы атомарны по своей природе. Они построены на основе набора функций. На третьей странице есть блок контента и интерактивная сова, поэтому для каждого из них есть свой компонент.

Блоки контента — это ссылки на контент, видимый в книге. Они также управляются объектом конфигурации.

{
 "contentBlocks": [
    {
      "id": "one",
      "title": "New in Chrome",
      "blurb": "Lift your spirits with a round up of all the tools and features in Chrome.",
      "link": "https://www.youtube.com/watch?v=qwdN1fJA_d8&list=PLNYkxOF6rcIDfz8XEA3loxY32tYh7CI3m"
    },
    …otherBlocks
  ]
}

Эта конфигурация импортируется там, где требуются блоки контента. Затем соответствующая конфигурация блока передается компоненту ContentBlock .

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

Здесь также приведен пример того, как мы используем компонент страницы в качестве места для позиционирования контента. Здесь позиционируется блок контента.

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

Однако общие стили блока контента совпадают с кодом компонента.

.content-block {
  background: hsl(0deg 0% 0% / 70%);
  color: var(--gray-0);
  border-radius:  min(3vh, var(--size-4));
  padding: clamp(0.75rem, 2vw, 1.25rem);
  display: grid;
  gap: var(--size-2);
  position: absolute;
  cursor: pointer;
  width: 50%;
}

Что касается нашей совы, то это интерактивная функция — одна из многих в этом проекте. Это хороший небольшой пример, показывающий, как мы использовали созданную общую ViewTimeline.

На высоком уровне наш компонент совы импортирует немного SVG и встраивает его с помощью Astro's Fragment.

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

А стили позиционирования нашей совы совмещены с кодом компонента.

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

Есть еще один дополнительный стиль, определяющий поведение transform совы.

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

Использование transform-box влияет на transform-origin . Это делается относительно ограничивающей рамки объекта в SVG. Сова увеличивается от нижнего центра, поэтому используется transform-origin: 50% 100% .

Самое интересное, когда мы связываем сову с одним из наших сгенерированных ViewTimeline :

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

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

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

В этом блоке кода мы делаем две вещи:

  1. Проверьте предпочтения пользователя в отношении движения.
  2. Если у них нет предпочтений, привяжите анимацию совы для прокрутки.

Во второй части сова анимируется по оси Y с помощью API веб-анимации. Используется индивидуальное свойство translate , которое связано с одним ViewTimeline . Он связан с CHROMETOBER_TIMELINES[1] через свойство timeline . Это ViewTimeline , созданный для перелистывания страниц. Это связывает анимацию совы с перелистыванием страницы с помощью фазы enter . Он определяет, что когда страница будет перевернута на 80%, начните перемещать сову. На 90% сова должна закончить свой перевод.

Особенности книги

Теперь вы познакомились с подходом к созданию страницы и с тем, как работает архитектура проекта. Вы можете увидеть, как это позволяет участникам сразу же приступить к работе над страницей или функцией по своему выбору. Анимация различных функций в книге связана с перелистыванием страниц книги; например, летучая мышь, которая летает и вылетает при переворачивании страницы.

Он также содержит элементы, основанные на CSS-анимации .

Как только блоки контента были в книге, появилось время проявить творческий подход к другим функциям. Это дало возможность создать несколько разных взаимодействий и попробовать разные способы реализации вещей.

Обеспечение оперативности реагирования

Адаптивные единицы просмотра позволяют изменить размер книги и ее функций. Однако обеспечение адаптивности шрифтов оказалось интересной задачей. Здесь хорошо подходят контейнерные запросы. Однако они пока поддерживаются не везде. Размер книги задан, поэтому запрос к контейнеру нам не нужен. Встроенный блок запроса контейнера можно создать с помощью CSS calc() и использовать для изменения размера шрифта.


.book-placeholder {
  --size: clamp(12rem, 72vw, 80vmin);
  --aspect-ratio: 360 / 504;
  --cqi: calc(0.01 * (var(--size) * (var(--aspect-ratio))));
}

.content-block h2 {
  color: var(--gray-0);
  font-size: clamp(0.6rem, var(--cqi) * 4, 1.5rem);
}

.content-block :is(p, a) {
  font-size: clamp(0.6rem, var(--cqi) * 3, 1.5rem);
}

Тыквы светятся ночью

Те, у кого острый глаз, возможно, заметили использование элементов <source> при обсуждении фона страницы ранее. Уне хотелось, чтобы взаимодействие реагировало на предпочтения цветовой схемы. В результате фоны поддерживают как светлый, так и темный режимы с различными вариантами. Поскольку вы можете использовать медиа-запросы с элементом <picture> , это отличный способ предоставить два стиля фона. Элемент <source> запрашивает предпочтения цветовой схемы и показывает соответствующий фон.

<picture>
  <source srcset={darkFront} media="(prefers-color-scheme: dark)" height="214" width="150">
  <img src={lightFront} class="page__background page__background--right" alt="" aria-hidden="true" height="214" width="150">
</picture>

Вы можете внести другие изменения в зависимости от предпочтений этой цветовой схемы. Тыквы на второй странице реагируют на предпочтения цветовой схемы пользователя. В используемом SVG есть круги, обозначающие пламя, которое масштабируется и анимируется в темном режиме.

.pumpkin__flame,
 .pumpkin__flame circle {
   transform-box: fill-box;
   transform-origin: 50% 100%;
 }

 .pumpkin__flame {
   scale: 0.8;
 }

 .pumpkin__flame circle {
   transition: scale 0.2s;
   scale: 0;
 }

@media(prefers-color-scheme: dark) {
   .pumpkin__flame {
     animation: pumpkin-flicker 3s calc(var(--index, 0) * -1s) infinite linear;
   }

   .pumpkin__flame circle {
     scale: 1;
   }

   @keyframes pumpkin-flicker {
     50% {
       scale: 1;
     }
   }
 }

Этот портрет наблюдает за тобой?

Если вы посмотрите страницу 10, вы можете кое-что заметить. За вами следят! Глаза портрета будут следовать за указателем при перемещении по странице. Хитрость здесь заключается в том, чтобы сопоставить местоположение указателя со значением перевода и передать его в CSS.

const mapRange = (inputLower, inputUpper, outputLower, outputUpper, value) => {
   const INPUT_RANGE = inputUpper - inputLower
   const OUTPUT_RANGE = outputUpper - outputLower
   return outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0)
 }

Этот код принимает входные и выходные диапазоны и отображает заданные значения. Например, такое использование даст значение 625.

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

Для портрета входным значением является центральная точка каждого глаза плюс или минус некоторое расстояние в пикселях. Выходной диапазон — это то, сколько глаз могут перевести в пикселях. И затем позиция указателя на оси x или y передается как значение. Чтобы получить центральную точку глаз при их перемещении, глаза дублируются. Оригиналы неподвижны, прозрачны и используются для справки.

Затем нужно связать все это вместе и обновить значения пользовательских свойств CSS для глаз, чтобы глаза могли двигаться. Функция привязана к событию pointermove для window . По мере того, как это срабатывает, границы каждого глаза привыкают к вычислению центральных точек. Затем положение указателя сопоставляется со значениями, которые установлены как значения настраиваемых свойств глаз.

const RANGE = 15
const LIMIT = 80
const interact = ({ x, y }) => {
   // map a range against the eyes and pass in via custom properties
   const LEFT_EYE_BOUNDS = LEFT_EYE.getBoundingClientRect()
   const RIGHT_EYE_BOUNDS = RIGHT_EYE.getBoundingClientRect()

   const CENTERS = {
     lx: LEFT_EYE_BOUNDS.left + LEFT_EYE_BOUNDS.width * 0.5,
     rx: RIGHT_EYE_BOUNDS.left + RIGHT_EYE_BOUNDS.width * 0.5,
     ly: LEFT_EYE_BOUNDS.top + LEFT_EYE_BOUNDS.height * 0.5,
     ry: RIGHT_EYE_BOUNDS.top + RIGHT_EYE_BOUNDS.height * 0.5,
   }

   Object.entries(CENTERS)
     .forEach(([key, value]) => {
       const result = mapRange(value - LIMIT, value + LIMIT, -RANGE, RANGE)(key.indexOf('x') !== -1 ? x : y)
       EYES.style.setProperty(`--${key}`, result)
     })
 }

Как только значения передаются в CSS, стили могут делать с ними все, что хотят. Самое интересное здесь — использование CSS- clamp() , чтобы сделать поведение каждого глаза разным, так что вы можете заставить каждый глаз вести себя по-разному, не касаясь повторно JavaScript.

.portrait__eye--mover {
   transition: translate 0.2s;
 }

 .portrait__eye--mover.portrait__eye--left {
   translate:
     clamp(-10px, var(--lx, 0) * 1px, 4px)
     clamp(-4px, var(--ly, 0) * 0.5px, 10px);
 }

 .portrait__eye--mover.portrait__eye--right {
   translate:
     clamp(-4px, var(--rx, 0) * 1px, 10px)
     clamp(-4px, var(--ry, 0) * 0.5px, 10px);
 }

Произнесение заклинаний

Если вы прочтете шестую страницу, почувствуете ли вы себя зачарованными? На этой странице представлен дизайн нашей фантастической волшебной лисы. Если вы переместите указатель, вы можете увидеть эффект пользовательского следа курсора. Здесь используется анимация холста. Элемент <canvas> располагается над остальным содержимым страницы с помощью pointer-events: none . Это означает, что пользователи по-прежнему могут нажимать на блоки контента внизу.

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

Подобно тому, как наш портрет слушает событие pointermove в window , то же самое делает и наш элемент <canvas> . Однако каждый раз, когда событие срабатывает, мы создаем объект для анимации в элементе <canvas> . Эти объекты представляют собой формы, используемые в следе курсора. У них есть координаты и случайный оттенок.

Наша предыдущая функция mapRange используется снова, поскольку мы можем использовать ее для сопоставления дельты указателя с size и rate . Объекты хранятся в массиве, который зацикливается, когда объекты рисуются в элементе <canvas> . Свойства каждого объекта сообщают нашему элементу <canvas> , где что следует рисовать.

const blocks = []
  const createBlock = ({ x, y, movementX, movementY }) => {
    const LOWER_SIZE = CANVAS.height * 0.05
    const UPPER_SIZE = CANVAS.height * 0.25
    const size = mapRange(0, 100, LOWER_SIZE, UPPER_SIZE, Math.max(Math.abs(movementX), Math.abs(movementY)))
    const rate = mapRange(LOWER_SIZE, UPPER_SIZE, 1, 5, size)
    const { left, top, width, height } = CANVAS.getBoundingClientRect()
    
    const block = {
      hue: Math.random() * 359,
      x: x - left,
      y: y - top,
      size,
      rate,
    }
    
    blocks.push(block)
  }
window.addEventListener('pointermove', createBlock)

Для рисования на холсте создается цикл с помощью requestAnimationFrame . След курсора должен отображаться только тогда, когда страница находится в поле зрения. У нас есть IntersectionObserver , который обновляет и определяет, какие страницы находятся в поле зрения. Если страница находится в поле зрения, объекты отображаются на холсте в виде кругов.

Затем мы перебираем массив blocks и рисуем каждую часть следа. Каждый кадр уменьшает размер и меняет положение объекта со rate . Это производит эффект падения и масштабирования. Если объект полностью сжимается, он удаляется из массива blocks .

let wandFrame
const drawBlocks = () => {
   ctx.clearRect(0, 0, CANVAS.width, CANVAS.height)
  
   if (PAGE_SIX.className.indexOf('in-view') === -1 && wandFrame) {
     blocks.length = 0
     cancelAnimationFrame(wandFrame)
     document.body.removeEventListener('pointermove', createBlock)
     document.removeEventListener('resize', init)
   }
  
   for (let b = 0; b < blocks.length; b++) {
     const block = blocks[b]
     ctx.strokeStyle = ctx.fillStyle = `hsla(${block.hue}, 80%, 80%, 0.5)`
     ctx.beginPath()
     ctx.arc(block.x, block.y, block.size * 0.5, 0, 2 * Math.PI)
     ctx.stroke()
     ctx.fill()

     block.size -= block.rate
     block.y += block.rate

     if (block.size <= 0) {
       blocks.splice(b, 1)
     }

   }
   wandFrame = requestAnimationFrame(drawBlocks)
 }

Если страница выходит из поля зрения, прослушиватели событий удаляются и цикл анимации отменяется. Массив blocks также очищается.

Вот след курсора в действии!

Обзор доступности

Это все хорошо — создавать интересный опыт для изучения, но бесполезно, если он недоступен для пользователей. Опыт Адама в этой области оказался неоценимым в подготовке Chrometober к проверке доступности перед выпуском.

Некоторые из примечательных областей охвачены:

  • Обеспечение семантики используемого HTML. Это включало в себя такие элементы, как соответствующие ориентиры, такие как <main> для книги; а также использование элемента <article> для каждого блока контента и элементов <abbr> , в которых вводятся аббревиатуры. Заблаговременное мышление по мере создания книги сделало ее более доступной. Использование заголовков и ссылок облегчает пользователю навигацию. Использование списка страниц также означает, что количество страниц объявляется с помощью вспомогательных технологий.
  • Обеспечение того, чтобы все изображения использовали соответствующие атрибуты alt . Для встроенных SVG-элементов title присутствует там, где это необходимо.
  • Использование атрибутов aria там, где они улучшают восприятие. Использование aria-label для страниц и их сторон сообщает пользователю, на какой странице они находятся. Использование aria-describedBy в ссылках «Читать дальше» передает текст блока контента. Это устраняет двусмысленность относительно того, куда ссылка приведет пользователя.
  • Что касается контентных блоков, то доступна возможность кликнуть по всей карточке, а не только по ссылке «Читать далее».
  • Использование IntersectionObserver для отслеживания того, какие страницы находятся в поле зрения, обсуждалось ранее. Это имеет множество преимуществ, которые связаны не только с производительностью. На страницах, которые не отображаются, анимация или взаимодействие будут приостановлены. Но к этим страницам также применен атрибут inert . Это означает, что пользователи, использующие программу чтения с экрана, могут просматривать тот же контент, что и зрячие пользователи. Фокус остается на просматриваемой странице, и пользователи не могут перейти на другую страницу.
  • И последнее, но не менее важное: мы используем медиа-запросы, чтобы учесть предпочтения пользователя в отношении движения.

Вот скриншот из обзора, на котором показаны некоторые принятые меры.

Этот элемент обозначен вокруг всей книги, что указывает на то, что он должен быть основным ориентиром для пользователей ассистивных технологий. Подробнее показано на скриншоте." width="800" height="465">

Скриншот открытой книги Chrometober. Зеленые рамки выделены вокруг различных аспектов пользовательского интерфейса, описывая предполагаемые функциональные возможности специальных возможностей и результаты взаимодействия с пользователем, которые обеспечит страница. Например, изображения имеют замещающий текст. Другой пример — метка доступности, объявляющая, что страницы, находящиеся вне поля зрения, являются инертными. Подробнее показано на скриншоте.

Что мы узнали

Мотивация создания Chrometober заключалась не только в том, чтобы привлечь внимание сообщества к веб-контенту, но и в том, что мы смогли протестировать полифил API для анимации со ссылками на прокрутку, который находится в разработке.

Во время саммита нашей команды в Нью-Йорке мы выделили сессию, чтобы протестировать проект и решить возникшие проблемы. Вклад команды был неоценим. Это также была прекрасная возможность составить список всех вещей, которые нужно было решить, прежде чем мы сможем начать работу.

Команда CSS, UI и DevTools сидит за столом в конференц-зале. Уна стоит у доски, исписанной стикерами. Остальные члены команды сидят за столом с прохладительными напитками и ноутбуками.

Например, при тестировании книги на устройствах возникла проблема с рендерингом. Наша книга не будет отображаться должным образом на устройствах iOS. Единицы области просмотра определяют размер страницы, но наличие выреза влияло на книгу. Решением было использовать viewport-fit=cover в meta окне просмотра:

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

На этом сеансе также были подняты некоторые проблемы с полифилом API. Брамус поднял эти проблемы в репозитории полифиллов. Впоследствии он нашел решения этих проблем и объединил их в полифилл. Например, этот запрос на включение позволил повысить производительность за счет добавления кэширования к части полифила.

Скриншот демо-версии, открытой в Chrome. Инструменты разработчика открыты и показывают базовые показатели производительности.

Скриншот демо-версии, открытой в Chrome. Инструменты разработчика открыты и демонстрируют улучшенные показатели производительности.

Вот и все!

Работать над этим проектом было очень интересно, результатом чего стала причудливая прокрутка, подчеркивающая потрясающий контент от сообщества. Мало того, это было здорово для тестирования полифила, а также для предоставления обратной связи команде разработчиков, чтобы помочь улучшить полифил.

Chrometober 2022 — это завершение.

Мы надеемся, что вам понравилось! Какая ваша любимая функция? Напишите мне в Твиттере и дайте нам знать!

Джей держит в руках лист наклеек с персонажами из «Хромтобера».

Возможно, вы даже сможете получить несколько наклеек от одного из членов команды, если увидите нас на мероприятии .

Фотография героя Дэвида Менидри на Unsplash