Как появилась на свет книга с прокруткой, благодаря которой этот Хромтобер поделился забавными и пугающими советами и приемами.
Вслед за 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 для привязки анимации к прокрутке. Важно отметить, что вы не можете активировать анимацию при прокрутке — это может появиться позже. Анимации, связанные с прокруткой, также делятся на две основные категории:
- Те, которые реагируют на положение прокрутки.
- Те, которые реагируют на положение элемента в его контейнере прокрутки.
Для создания последнего мы используем 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 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()
В этом блоке кода мы делаем две вещи:
- Проверьте предпочтения пользователя в отношении движения.
- Если у них нет предпочтений, привяжите анимацию совы для прокрутки.
Во второй части сова анимируется по оси 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 заключалась не только в том, чтобы привлечь внимание сообщества к веб-контенту, но и в том, что это был для нас способ протестировать полифил API для анимации со ссылками на прокрутку, который находится в разработке.
Во время саммита нашей команды в Нью-Йорке мы выделили сессию, чтобы протестировать проект и решить возникшие проблемы. Вклад команды был неоценим. Это также была прекрасная возможность составить список всех вещей, которые нужно было решить, прежде чем мы сможем начать работу.
Например, при тестировании книги на устройствах возникла проблема с рендерингом. Наша книга не будет отображаться должным образом на устройствах iOS. Единицы области просмотра определяют размер страницы, но наличие выреза влияло на книгу. Решением было использовать viewport-fit=cover
в meta
просмотра:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
На этом сеансе также были подняты некоторые проблемы с полифилом API. Брамус поднял эти проблемы в репозитории полифиллов. Впоследствии он нашел решения этих проблем и объединил их в полифилл. Например, этот запрос на включение позволил повысить производительность за счет добавления кэширования к части полифила.
Вот и все!
Работать над этим проектом было очень интересно, результатом чего стала причудливая прокрутка, подчеркивающая потрясающий контент от сообщества. Мало того, это было здорово для тестирования полифила, а также для предоставления обратной связи команде разработчиков, чтобы помочь улучшить полифил.
Chrometober 2022 — это завершение.
Мы надеемся, что вам понравилось! Какая ваша любимая функция? Напишите мне в Твиттере и дайте нам знать!
Возможно, вы даже сможете получить несколько наклеек от одного из членов команды, если увидите нас на мероприятии .
Фотография героя Дэвида Менидри на Unsplash
,Как появилась на свет книга с прокруткой, благодаря которой этот Хромтобер поделился забавными и пугающими советами и приемами.
Вслед за 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 для привязки анимации к прокрутке. Важно отметить, что вы не можете активировать анимацию при прокрутке — это может появиться позже. Анимации, связанные с прокруткой, также делятся на две основные категории:
- Те, которые реагируют на положение прокрутки.
- Те, которые реагируют на положение элемента в его контейнере прокрутки.
Для создания последнего мы используем 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 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, но у них есть кодовый забор вверху, похожий на FrontMatter. Это позволяет нам делать такие вещи, как импортировать другие компоненты. Компонент для третьей страницы выглядит следующим образом:
---
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.
---
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
S:
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()
В этом блоке кода мы делаем две вещи:
- Проверьте на предпочтения пользователя.
- Если у них нет предпочтений, свяжите анимацию совы с прокруткой.
Во второй части сова анимирует по оси Y, используя API веб-анимации. Используется индивидуальное свойство translate
и связано с одной ViewTimeline
. Он связан с CHROMETOBER_TIMELINES[1]
через свойство timeline
. Это ViewTimeline
, которая генерируется для поворотов страницы. Это ссылается на анимацию OWL с поворотом страницы с помощью фазы enter
. Это определяет, что, когда страница повернута на 80%, начните перемещать сову. На 90%сова должна закончить свой перевод.
Книжные особенности
Теперь вы видели подход для создания страницы и того, как работает архитектура проекта. Вы можете увидеть, как это позволяет участникам прыгать и работать на странице или функции по своему выбору. Различные функции в книге имеют свои анимации, связанные с поворотом страницы книги; Например, летучая мышь, которая летает и вылетает на странице, поворачивается.
У этого также есть элементы, которые основаны на анимации CSS .
Как только блоки контента были в книге, было время проявить творческий подход с другими функциями. Это дало возможность генерировать некоторые различные взаимодействия и попробовать разные способы реализации вещей.
Сохраняя реагирующие вещи
Отзывчивые подразделения Viewport Размер книги и ее функции. Тем не менее, поддерживать отзывчивые шрифты было интересной проблемой. Контейнерные подразделения здесь хорошо подходят. Они еще не поддерживаются повсюду, хотя. Размер книги установлен, поэтому нам не нужен контейнерный запрос. Встроенный блок запроса контейнера может быть сгенерирован с помощью 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>
Вы можете ввести другие изменения, основанные на предпочтениях цветовой схемы. Тыквы на странице 2 -й на странице реагируют на предпочтения цветовой схемы пользователя. Используемый 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
из более раннего используется снова, так как мы можем использовать ее для составления карты Delta Delta по 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>
для книги; ASO Использование элемента<article>
для каждого блока контента и элементов<abbr>
, где вводятся аббревиатуры. Думая заранее, когда книга была построена, сделала вещи более доступными. Использование заголовков и ссылок облегчает навигацию пользователя. Использование списка для страниц также означает, что количество страниц объявляется вспомогательными технологиями. - Обеспечение того, чтобы все изображения использовали соответствующие атрибуты
alt
. Для встроенных SVGS элементtitle
присутствует, где это необходимо. - Использование атрибутов
aria
, где они улучшают опыт. Использованиеaria-label
для страниц и их сторон сообщает пользователю, на какую страницу он находится. Использованиеaria-describedBy
по ссылкам «Подробнее» передает текст блока контента. Это устраняет неоднозначность в отношении того, где ссылка примет пользователя. - На тему блоков контента, возможность щелкнуть всю карту и не только ссылку «Читать больше» доступна.
- Использование
IntersectionObserver
для отслеживания того, какие страницы видны, появилось ранее. У этого есть много преимуществ, которые не просто связаны с производительностью. Страницы, не имеющие в виду, будут иметь приостановку анимации или взаимодействия. Но эти страницы также применяютсяinert
атрибуты. Это означает, что пользователи, использующие читатель экрана, могут исследовать тот же контент, что и зрячитые пользователи. Фокус остается на странице, которая присутствует, и пользователи не могут вмешаться на другую страницу. - И последнее, но не менее важное, мы используем медиа -запросы, чтобы уважать предпочтение пользователя двигаться.
Вот скриншот из обзора, подчеркивающего некоторые меры.
Элемент идентифицируется как во всей книге, что указывает на то, что он должен быть основным ориентиром для пользователей вспомогательных технологий. Больше указано на скриншоте. "Width =" 800 "Height =" 465 ">
Что мы узнали
Мотивация Chrometober заключалась не только в том, чтобы выделить веб-контент от сообщества, но и для нас также протестировать пособие API-полифилла с анимацией, связанными с прокрутками, который находится в разработке.
Мы отказались от сессии на саммите нашей команды в Нью -Йорке, чтобы проверить проект и решить проблемы, которые возникли. Вклад команды был неоценимым. Это была также прекрасная возможность перечислить все, что нужно было решить, прежде чем мы сможем жить.
Например, тестирование книги на устройствах подняло проблему рендеринга. Наша книга не будет отображаться, как и ожидалось на устройствах iOS. Размер единиц Viewport на странице, но когда присутствовал выемка, это повлияло на книгу. Решением было использование viewport-fit=cover
в meta
Viewport:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
Эта сессия также подняла некоторые проблемы с полифиллом API. Брмус поднял эти проблемы в полифилл -репозитории. Впоследствии он обнаружил решения этих проблем и объединился в полифилл. Например, этот запрос на то, чтобы получить усиление производительности, добавив кэширование к части полифилла.
Вот и все!
Это был настоящий забавный проект, над которым можно было бы работать, в результате чего причудливый опыт прокрутки, который подчеркивает удивительный контент от сообщества. Мало того, это было отлично подходит для тестирования полифилла, а также для предоставления обратной связи команде инженеров, чтобы помочь улучшить полифилл.
Chrometober 2022 - это обертка.
Мы надеемся, что вам понравилось! Какая ваша любимая особенность? Твитнуть меня и дайте нам знать!
Возможно, вы даже сможете взять наклейки от одной из команды, если увидите нас на мероприятии .
Фото героя Дэвида Менидри на Unsplash
,Как прокрутка книга ожила за то, что поделилась веселыми и пугающими советами и хитростями этого хрометобера.
Следуя Designcember , мы хотели создать для вас Chrometober для вас в этом году, чтобы выделить и поделиться веб -контентом от команды сообщества и Chrome. Designcember продемонстрировал использование контейнерных запросов, но в этом году мы демонстрируем API анимации, связанные с прокруткой CSS.Проверьте опыт прокрутки книги на Web.dev/chrometober-2022 .
Обзор
Целью проекта было предоставление причудливого опыта, подчеркивающего API анимации, связанные с прокруткой. Но, будучи причудливым, опыт должен быть отзывчивым и доступным. Проект также был отличным способом протестирования вождения полифилла API, который находится в активной разработке; Это, а также пробовать разные методы и инструменты в комбинации. И все с праздничной темой Хэллоуина!
Наша командная структура выглядела так:
- Тайлер Рид : иллюстрация и дизайн
- Jhey Tompkins : архитектурное и творческое лидерство
- Уна Краветс : лидер проекта
- Bramus van Damme : участник сайта
- Адам Аргайл : обзор доступности
- Аарон Форинтон: копирайтинг
Составление опыта
Идеи для Chrometober начали течь в нашей первой команде за пределами места в мае 2022 года. Коллекция Scribbles заставила нас задуматься о том, как пользователь мог прокрутить свой путь по какой -то форме раскадровки. Вдохновленные видеоиграми, мы рассмотрели опыт прокрутки в таких сценах, как кладбище и дом с привидениями.
Было интересно иметь творческую свободу, чтобы взять мой первый проект Google в неожиданном направлении. Это был ранний прототип того, как пользователь мог бы перемещаться по контенту.
Когда пользователь прокручивается вбок, блоки вращаются и масштабируются. Но я решил уйти от этой идеи из -за беспокойства о том, как мы можем сделать этот опыт отличным для пользователей на устройствах всех размеров. Вместо этого я склонялся к дизайну того, что сделал в прошлом. В 2020 году мне повезло, что у меня был доступ к Scrolltrigger Greensock для создания демонстраций выпуска.
Одной из демонстраций, которую я построил, была книга 3D-CSS, где страницы повернулись, когда вы прокручиваете, и это было намного более подходящим для того, что мы хотели для Chrometober. API API-анимации, связанные с прокруткой, является идеальным обменом для этой функции. Это также хорошо работает с scroll-snap
, как вы увидите!
Наш иллюстратор проекта, Тайлер Рид , отлично изменил дизайн, когда мы изменили идеи. Тайлер проделал фантастическую работу, взяв все творческие идеи, брошенные на него и воплотив их в жизнь. Это было очень весело и идеи мозгового штурма вместе. Большая часть того, как мы хотели, чтобы это работало, заключалась в том, что функции разбились на изолированные блоки. Таким образом, мы могли бы составить их в сцены, а затем выбрать и выбрать то, что мы воплотили в жизнь.
Основная идея заключалась в том, что, поскольку пользователь пробирался через книгу, он мог получить доступ к блокам контента. Они также могут взаимодействовать с прихотьми, включая пасхальные яйца, которые мы встроили в этот опыт; Например, портрет в доме с привидениями, чьи глаза последовали за вашим указателем, или тонкие анимации, вызванные медиа -запросами. Эти идеи и функции будут анимированы на свитке. Ранняя идея была кроликом зомби, который поднимется и переведет вдоль оси X на свитке пользователя.
Познакомьтесь с API
Прежде чем мы смогли начать играть с отдельными функциями и пасхальными яйцами, нам нужна была книга. Таким образом, мы решили превратить это в шанс проверить функции для новых API API-анимации CSS . API API-анимации, связанные с прокруткой, в настоящее время не поддерживается ни в одном браузерах. Однако, разрабатывая API, инженеры в команде взаимодействия работали над полифиллом . Это обеспечивает способ проверить форму API по мере развития. Это означает, что мы могли бы использовать этот API сегодня, и такие веселые проекты часто являются отличным местом, чтобы попробовать экспериментальные функции и обеспечить обратную связь. Узнайте, что мы узнали, и отзывы, которые мы смогли предоставить, позже в статье .
На высоком уровне вы можете использовать этот API для связи анимации для прокрутки. Важно отметить, что вы не можете вызвать анимацию на свитке - это то, что может произойти позже. Анимации, связанные с прокруткой, также делятся на две основные категории:
- Те, которые реагируют на положение прокрутки.
- Те, которые реагируют на позицию элемента в его контейнере прокрутки.
Чтобы создать последнее, мы используем 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
Element. Это хорошо для сценариев, таких как анимация списка. Поведение похоже на то, как вы можете оживить элементы при въезде с использованием 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 очень хорошо работает с Scroll-Snap . Scroll-Snap в сочетании с 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, но у них есть кодовый забор вверху, похожий на FrontMatter. Это позволяет нам делать такие вещи, как импортировать другие компоненты. Компонент для третьей страницы выглядит следующим образом:
---
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.
---
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
S:
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()
В этом блоке кода мы делаем две вещи:
- Проверьте на предпочтения пользователя.
- Если у них нет предпочтений, свяжите анимацию совы с прокруткой.
Во второй части сова анимирует по оси Y, используя API веб-анимации. Используется индивидуальное свойство translate
и связано с одной ViewTimeline
. Он связан с CHROMETOBER_TIMELINES[1]
через свойство timeline
. Это ViewTimeline
, которая генерируется для поворотов страницы. Это ссылается на анимацию OWL с поворотом страницы с помощью фазы enter
. Это определяет, что, когда страница повернута на 80%, начните перемещать сову. На 90%сова должна закончить свой перевод.
Книжные особенности
Теперь вы видели подход для создания страницы и того, как работает архитектура проекта. Вы можете увидеть, как это позволяет участникам прыгать и работать на странице или функции по своему выбору. Различные функции в книге имеют свои анимации, связанные с поворотом страницы книги; Например, летучая мышь, которая летает и вылетает на странице, поворачивается.
У этого также есть элементы, которые основаны на анимации CSS .
Как только блоки контента были в книге, было время проявить творческий подход с другими функциями. Это дало возможность генерировать некоторые различные взаимодействия и попробовать разные способы реализации вещей.
Сохраняя реагирующие вещи
Отзывчивые подразделения Viewport Размер книги и ее функции. Тем не менее, поддерживать отзывчивые шрифты было интересной проблемой. Контейнерные подразделения здесь хорошо подходят. Они еще не поддерживаются повсюду, хотя. Размер книги установлен, поэтому нам не нужен контейнерный запрос. Встроенный блок запроса контейнера может быть сгенерирован с помощью 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>
Вы можете ввести другие изменения, основанные на предпочтениях цветовой схемы. Тыквы на странице 2 -й на странице реагируют на предпочтения цветовой схемы пользователя. Используемый 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
из более раннего используется снова, так как мы можем использовать ее для составления карты Delta Delta по 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>
для книги; ASO Использование элемента<article>
для каждого блока контента и элементов<abbr>
, где вводятся аббревиатуры. Думая заранее, когда книга была построена, сделала вещи более доступными. Использование заголовков и ссылок облегчает навигацию пользователя. Использование списка для страниц также означает, что количество страниц объявляется вспомогательными технологиями. - Обеспечение того, чтобы все изображения использовали соответствующие атрибуты
alt
. Для встроенных SVGS элементtitle
присутствует, где это необходимо. - Использование атрибутов
aria
, где они улучшают опыт. Использованиеaria-label
для страниц и их сторон сообщает пользователю, на какую страницу он находится. Использованиеaria-describedBy
по ссылкам «Подробнее» передает текст блока контента. Это устраняет неоднозначность в отношении того, где ссылка примет пользователя. - На тему блоков контента, возможность щелкнуть всю карту и не только ссылку «Читать больше» доступна.
- Использование
IntersectionObserver
для отслеживания того, какие страницы видны, появилось ранее. У этого есть много преимуществ, которые не просто связаны с производительностью. Страницы, не имеющие в виду, будут иметь приостановку анимации или взаимодействия. Но эти страницы также применяютсяinert
атрибуты. Это означает, что пользователи, использующие читатель экрана, могут исследовать тот же контент, что и зрячитые пользователи. Фокус остается на странице, которая присутствует, и пользователи не могут вмешаться на другую страницу. - И последнее, но не менее важное, мы используем медиа -запросы, чтобы уважать предпочтение пользователя двигаться.
Вот скриншот из обзора, подчеркивающего некоторые меры.
Элемент идентифицируется как во всей книге, что указывает на то, что он должен быть основным ориентиром для пользователей вспомогательных технологий. Больше указано на скриншоте. "Width =" 800 "Height =" 465 ">
Что мы узнали
The motivation behind Chrometober was not only to highlight web content from the community, but was also a way for us to test drive the scroll-linked animations API polyfill that's in development.
We set aside a session while on our team summit in New York to test the project and tackle issues that arose. The team's contribution was invaluable. It was also a great opportunity to list all the things that needed tackling before we could go live.
For example, testing out the book on devices raised a rendering issue. Our book wouldn't render as expected on iOS devices. Viewport units size the page, but when a notch was present, it affected the book. The solution was to use viewport-fit=cover
in the meta
viewport:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
This session also raised some issues with the API polyfill. Bramus raised these issues in the polyfill repository. He subsequently found solutions to those issues and got them merged into the polyfill. For example, this pull request made a performance gain by adding caching to part of the polyfill.
Вот и все!
This has been a real fun project to work on, resulting in a whimsical scrolling experience that highlights amazing content from the community. Not only that, it's been great for testing the polyfill, as well as providing feedback to the engineering team to help improve the polyfill.
Chrometober 2022 is a wrap.
Мы надеемся, что вам понравилось! What's your favorite feature? Tweet me and let us know!
You might even be able to grab some stickers from one of the team if you see us at an event .
Hero Photo by David Menidrey on Unsplash