Мы недавно выпустили новую и улучшенную версию Connect, нашего набора инструментов для платформ и магазинов. Группа дизайна Stripe много работала для создания уникальных посадочных страничек, которые рассказывают историю для наших основных продуктов. К релизу мы подготовили посадочную страничку Connect, чтобы отразить эти замысловатые, передовые возможности, но в то же время не утратив ясности и простоты изложения.
В этой статье мы опишем, как использовали несколько веб-технологий следующего поколения, чтобы запустить Connect, и пройдёмся по некоторым мелким техническим деталям нашего фронтенд-путешествия.
CSS Grid Layout
Ранее в этом году три основных браузера (Firefox, Chrome и Safari) почти одновременно выкатили свои реализации нового модуля CSS Grid Layout. Эти спецификации дают разработчикам двухмерную макетную систему, которая проста в использовании и невероятно мощная. Посадочная страница Connect полагается на сетки CSS практически везде, что делает некоторые на первый взгляд хитрые дизайнерские решения банально простыми в реализации. В качестве примера, давайте спрячем содержимое заголовка и сфокусируемся на фоне:
Исторически, мы создавали такие фоновые полосы (страйпы) с помощью абсолютного позиционирования, чтобы точно разместить каждый страйп на странице. Этот способ работает, но хрупкое позиционирование часто ведёт к небольшим проблемкам: например, из-за ошибок округления между полосами может образоваться зазор 1 px. Таблицы стилей к тому же быстро разбухают, и их труднее поддерживать, поскольку медийные запросы усложняются для учёта различий фона на разных размерах экрана.
CSS Grid устраняет практически все эти проблемы. Мы просто определяем гибкую сетку и размещаем страйпы в соответствующие ячейки. В Firefox есть удобный инспектор сеток, который визуализирует структуру вашего макета. Посмотрим, как он выглядит:
Мы выделили три страйпа и убрали эффект наклона для наглядности. Вот как будет выглядеть соответствующий код CSS:
header .stripes {
display: grid;
grid: repeat(5, 200px) / repeat(10, 1fr);
}
header .stripes :nth-child(1) {
grid-column: span 3;
}
header .stripes :nth-child(2) {
grid-area: 3 / span 3 / auto / -1;
}
header .stripes :nth-child(3) {
grid-row: 4;
grid-column: span 5;
}
Затем мы можем просто трансформировать весь контейнер .stripes
для получения эффекта наклона:
header .stripes {
transform: skewY(-12deg);
transform-origin: 0;
}
И вуаля! CSS Grid может испугать с первого взгляда, поскольку сопровождается необычным синтаксисом и многими новыми свойствами и значениями, но ментальная модель на самом деле очень простая. И если вы знакомы с Flexbox, то уже знаете модуль Box Alignment, а значит, можете здесь тоже использовать знакомые свойства, которые так любите, такие как justify-content
и align-items
.
CSS 3D
Заголовок целевой страницы показывает несколько кубов как визуальную метаформу строительных блоков, которые составляют Connect. Эти летающие кубы вращаются в 3D на случайных скоростях (в определённом диапазоне) и освещаются одним источником света, который динамически подсвечивает соответствующие поверхности, видео: cubes.mp4
Эти кубы — простые элементы DOM, которые генерируются и анимируются средствами JavaScript. Каждый из них подтверждается одним HTML template
:
<!-- HTML -->
<template id="cube-template">
<div class="cube">
<div class="shadow"></div>
<div class="sides">
<div class="back"></div>
<div class="top"></div>
<div class="left"></div>
<div class="front"></div>
<div class="right"></div>
<div class="bottom"></div>
</div>
</div>
</template>
// JavaScript
const createCube = () => {
const template = document.getElementById("cube-template");
const fragment = document.importNode(template.content, true);
return fragment;
};
Ничего сложного. Теперь мы можем довольно легко превратить эти чистые и пустые элементы в трёхмерную форму. Благодаря 3D-трансформациям добавление перспективы и перемещение сторон вдоль z-осей осуществляется вполне естественным образом:
.cube, .cube * {
position: absolute;
width: 100px;
height: 100px
}
.sides {
transform-style: preserve-3d;
perspective: 600px
}
.front { transform: rotateY(0deg) translateZ(50px) }
.back { transform: rotateY(-180deg) translateZ(50px) }
.left { transform: rotateY(-90deg) translateZ(50px) }
.right { transform: rotateY(90deg) translateZ(50px) }
.top { transform: rotateX(90deg) translateZ(50px) }
.bottom { transform: rotateX(-90deg) translateZ(50px) }
Хотя CSS делает тривиальным моделирование куба, но он не даёт продвинутых функций анимации, такие как динамическое затенение. Вместо этого анимация кубов полагается на requestAnimationFrame
для вычисления и обновления каждой стороны в любой точке вращения. В каждом кадре нужно определить три вещи:
- Видимость. В каждый момент времени видимы не более трёх сторон, так что можно избежать лишних вычислений и ресурсоёмких перекрашиваний на скрытых сторонах.
- Трансформация. Каждая сторона куба должна быть преобразована в зависимости от своего первоначального вращения, текущего состояния анимации и скорости по каждой оси.
- Затенение. Хотя CSS позволяет вам позиционировать элементы в трёхмерном пространстве, здесь нет традиционных понятий из 3D-среды (например, источников света). Чтобы имитировать 3D-среду, мы можем рендерить источник света, прогрессивно затемняя стороны куба по мере того, как он удаляется от определённой точки.
Есть и другие соображения, которые нужно принять в расчёт (например, улучшение производительности с помощью requestIdleCallback
на JavaScript и backface-visibility
на CSS), но это главные основы для логики анимации.
Мы можем вычислить видимость и трансформацию для каждой стороны, непрерывно отслеживая их состояния и обновляя их с помощью простых математических операций. При использовании чистых функций и фич ES2015, таких как шаблонные литералы, всё становится даже проще. Вот два коротких фрагмента кода JavaScript для вычисления и определения текущей трансформации:
const getDistance = (state, rotate) =>
["x", "y"].reduce((object, axis) => {
object[axis] = Math.abs(state[axis] + rotate[axis]);
return object;
}, {});
const getRotation = (state, size, rotate) => {
const axis = rotate.x ? "Z" : "Y";
const direction = rotate.x > 0 ? -1 : 1;
return `
rotateX(${state.x + rotate.x}deg)
rotate${axis}(${direction * (state.y + rotate.y)}deg)
translateZ(${size / 2}px)
`;
};
Самый сложный кусочек паззла — как правильно вычислить затенение для каждой стороны куба. Чтобы имитировать виртуальный источник света в центре сцены, мы можем постепенно увеличивать эффект освещения каждой стороны по мере того, как она приближается к центральной точке — по всем осям. Конкретно, это означает, что нам нужно вычислять яркость и цвет для каждой стороны. Будем выполнять это вычисление в каждом кадре, интерполируя базовый цвет и текущий фактор затенения.
// Linear interpolation between a and b
// Example: (100, 200, .5) = 150
const interpolate = (a, b, i) => a * (1 - i) + b * i;
const getShading = (tint, rotate, distance) => {
const darken = ["x", "y"].reduce((object, axis) => {
const delta = distance[axis];
const ratio = delta / 180;
object[axis] = delta > 180 ? Math.abs(2 - ratio) : ratio;
return object;
}, {});
if (rotate.x)
darken.y = 0;
else {
const {x} = distance;
if (x > 90 && x < 270)
directions.forEach(axis => darken[axis] = 1 - darken[axis]);
}
const alpha = (darken.x + darken.y) / 2;
const blend = (value, index) =>
Math.round(interpolate(value, tint.shading[index], alpha));
const [r, g, b] = tint.color.map(blend);
return `rgb(${r}, ${g}, ${b})`;
};
Фух! К счастью, остальной код гораздо проще и состоит, в основном, из шаблонного кода, DOM-хелперов и других элементарных абстракций. Последняя достойная упоминания деталь — это техника, которая делает анимацию менее навязчивой, в зависимости от настроек пользователя: видео.
На macOS, когда в настройках включен режим Reduce Motion, сработает триггер на новый медиазапрос prefers-reduced-motion
(пока только в Safari), и все декоративные анимации на странице отключатся. Кубы одновременно используют анимации CSS для затенения и анимации JavaScript для вращения. Мы можем отключить эти анимации сочетанием блокировок @media
и MediaQueryList Interface
:
/* CSS */
@media (prefers-reduced-motion) {
#header-hero * {
animation: none
}
}
// JavaScript
const reduceMotion = matchMedia("(prefers-reduced-motion)").matches;
const tick = () => {
cubes.forEach(updateSides);
if (reduceMotion) return;
requestAnimationFrame(tick);
};
Ещё больше CSS 3D!
По всему сайту мы используем кастомные трёхмерные компьютерные устройства как витрину для клиентов Stripe и имеющихся приложений. В нашем бесконечном квесте по уменьшению размера файлов и времени загрузки мы рассмотрели несколько вариантов, как добиться трёхмерного вида при малом размере файла и независимости от разрешения. Отрисовка устройств напрямую в CSS удовлетворила нашим требованиям. Вот CSS-ноутбук:
Определение объекта в CSS определённо менее удобно, чем экспорт битмапа, но это стоит того. Ноутбук вверху занимает меньше одного килобайта и его легко настраивать. Мы можем добавить аппаратное ускорение, анимировать любую часть, сделать его интерактивным без потери качества изображения и точно позиционировать DOM-элементы (например, другие изображения) на дисплее ноутбука. Такая гибкость не означает, что нужно отказаться чистого кода — разметка остаётся чистой, лаконичной и наглядной:
<div class="laptop">
<span class="shadow"></span>
<span class="lid"></span>
<span class="camera"></span>
<span class="screen"></span>
<span class="chassis">
<span class="keyboard"></span>
<span class="trackpad"></span>
</span>
</div>
Стилизация ноутбука включает в себя смесь градиентов, теней и трансформаций. Во многих отношениях это простая трансляция рабочего процесса и концепций, которые вы знаете и используете в своих графических инструментах. Например, вот код CSS для крышки:
.laptop .lid {
position: absolute;
width: 100%;
height: 100%;
border-radius: 20px;
background: linear-gradient(45deg, #E5EBF2, #F3F8FB);
box-shadow: inset 1px -4px 6px rgba(145, 161, 181, .3)
}
Выбор правильного инструмента для работы не всегда очевиден — выбор между CSS, SVG, Canvas, WebGL и изображениями не такой ясный, каким должен быть. Легко отказаться от CSS как эксклюзивного формата представления документов, но настолько же легко выйти за рамки и излишне использовать его визуальные возможности. Неважно, какую технологию вы выбрали, оптимизируйте её для пользователя! Значит, уделяйте пристальное внимание производительности на стороне клиента, доступности и опциям отката для старых браузеров.
Web Animations API
В разделе Onboarding & Verification представлено демо Express, новой системы адаптации начинающих пользователей Connect. Вся анимация целиком построена на программном коде и в основном полагается на новые Web Animations API.
Web Animations API обеспечивают производительность и простоту @keyframes
в JavaScript, позволяя легко создавать плавную последовательность кадров анимации. В отличие от низкоуровневых интерфейсов requestAnimationFrame
, здесь вы получаете все прелести анимаций CSS, такие как нативная поддержка смягчающих функций cubic-bezier
. В качестве примера, взглянем на наш код для скольжения клавиатуры:
const toggleKeyboard = (element, callback, action) => {
const keyframes = {
transform: [100, 0].map(n => `translateY(${n}%)`)
};
const options = {
duration: 800,
fill: "forwards",
easing: "cubic-bezier(.2, 1, .2, 1)",
direction: action == "hide" ? "reverse" : "normal"
};
const animation = element.animate(keyframes, options);
animation.addEventListener("finish", callback, {once: true});
};
Приятно и просто! Web Animations API покрывают абсолютное большинство типичных анимаций пользовательского интерфейса, какие только могут понадобиться, не имея сторонних зависимостей (в результате, вся анимация Express занимает около 5 КБ, включая всё: скрипты, изображения и т. д.). Нужно сказать, что это не полная замена requestAnimationFrame
, там всё-таки предоставляется более тонкий контроль над анимацией и допускаются эффекты, которые иначе не получишь, такие как Spring Curve и независимые функции трансформации. Если вы не уверены, какую технологию выбрать для своих анимаций, то вероятно, варианты можно расставить в следующем приоритетном порядке:
- Переходы CSS. Это самый быстрый, простой и наиболее эффективный способ анимаций. Подходит для простых вещей вроде эффектов
hover
. - Анимации CSS. У них такие же характеристики производительности, как у переходов: это декларативные анимации, которые могут быть сильно оптимизированы браузерами и выполняться в отдельных потоках. Анимации CSS более функциональны, чем переходы, и допускают множественные шаги и множественные итерации. Они также более сложны в реализации, поскольку требуют именованных деклараций
@keyframes
, а часто и явногоanimation-fill-mode
. (А именованные штуки всегда были сложнейшими частями компьютерной науки!) - Web Animations API. Этот программный интерфейс предоставляет почти такую же производительность, как анимации CSS (эти анимации проводит тот же движок, но код JavaScript по-прежнему работает в основном потоке), и их почти так же просто использовать. Они должны быть вашим первым выбором для любой анимации, где нужна интерактивность, случайные эффекты, программируемые последовательности и всё, что функциональнее чисто декларативной анимации.
- requestAnimationFrame. Во вселенной нет границ, но вам нужно построить космический корабль. Здесь возможности бесконечны, а методы рендеринга неограничены (HTML, SVG, canvas — что угодно), но эту технологию гораздо сложнее использовать и она может работать не так хорошо, как предыдущие варианты.
Вне зависимости от того, какую технику вы используете, вот несколько простых советов, которые вы всегда можете использовать, чтобы ваша анимация выглядела значительно лучше:
- Кастомные кривые. Вряд ли вам захочется использовать встроенные
timing-function
вродеease-in
,ease-out
иlinear
. Вы сэкономите много времени, если глобально определите количество кастомных переменных cubic-bezier. - Производительность. Всеми силами избегайте подтормаживаний в своих анимациях. В CSS для этого следует эксклюзивно анимировать дешёвые свойства (
transform
иopacity
) и сбрасывать анимации на GPU по возможности (применяяwill-change
). - Скорость. Анимации никогда не должны мешать. Основная задача анимаций — сделать интерфейс отзывчивым, гармоничным, приятным и завершённым. Нет жёсткого ограничения на продолжительность анимации, это зависит от эффектов и кривой, но в большинстве случаев вам не стоит превышать 500 секунд.
Intersection Observer
Воспроизведение анимации Express начинается автоматически как только она появляется в поле видимости (вы можете испытать это, прокручивая страничку). Обычно это сопровождается наблюдением за скроллингом, который срабатывает как триггер, но исторически это реализовали через ресурсоёмкие слушатели событий, что приводило к многословному и неэффективному коду.
Целевая страница Connect использует новый Intersection Observer API, который обеспечивает намного, намного более надёжный и производительный способ определять видимость элемента. Вот как мы начинаем воспроизводить анимацию Express:
const observeScroll = (element, callback) => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.intersectionRatio < 1) return;
callback();
// Stop watching the element
observer.disconnect();
},{
threshold: 1
});
// Start watching the element
observer.observe(element);
};
const element = document.getElementById("express-animation");
observeScroll(element, startAnimation);
Хелпер observeScroll
упрощает нам детектирование (например, когда элемент полностью видим, то обратный вызов генерируется лишь однажды) без выполнения какого-либо кода в основном потоке. Благодаря Intersection Observer API мы теперь на шаг ближе к абсолютно плавным веб-страницам!
Полифиллы и откаты
Все эти новенькие и блестящие программные интерфейсы — очень замечательно, но к сожалению, они пока не доступны повсеместно. Типичным обходным манёвром является использование полифилла, который проверяет присутствие фичи для конкретного API и исполняется только в случае отсутствия этого API. Очевидным недостатком такого подхода является то, что он отнимает ресурсы у всех и всегда, заставляя всех скачивать полифилл, независимо от того, будет тот использоваться или нет. Мы выбрали другое решение.
Для JavaScript APIs целевая страница Connect выполняет тест, нужен ли полифилл, и может динамически подгрузить его на страницу. Скрипты динамически создаются и добавляются к документу, они асинхронные по умолчанию, то есть порядок выполнения не гарантируется. Очевидно, это является проблемой, поскольку данный скрипт может выполниться раньше, чем ожидаемый полифилл. К счастью, это можно исправить явным указанием на то, что наши скрипты не асинхронные и поэтому лениво подгружаются только в случае необходимости:
const insert = name => {
const el = document.createElement("script");
el.src = `${name}.js`;
el.async = false; // Keep the execution order
document.head.appendChild(el);
};
const scripts = ["main"];
if (!Element.prototype.animate)
scripts.unshift("web-animations-polyfill");
if (!("IntersectionObserver" in window))
scripts.unshift("intersection-observer-polyfill");
scripts.forEach(insert);
Для CSS проблема и решения во многом такие же, как для полифиллов JavaScript. Типичный способ использования современных функций CSS — сначала написать откат, а затем перекрывать его, если возможно:
div { display: flex }
@supports (display: grid) {
div { display: grid }
}
Запросы функций CSS простые, надёжные и, скорее всего, их следует использовать в первую очередь. Однако они не подходят для нашей аудитории, потому что около 90% наших посетителей уже используют Grid-совместимый браузер. В нашем случае нет смысла штрафовать абсолютное большинство посетителей сотнями правил отката ради маленькой и уменьшающейся доли браузеров. С учётом этой статистики, мы выбрали динамическое создание и вставку таблицы стилей с откатом, когда необходимо:
// Some browsers not supporting Grid don’t support CSS.supports
// either, so we need to feature-test it the old-fashioned way:
if (!("grid" in document.body.style)) {
const fallback = "<link rel=stylesheet href=fallback.css>";
document.head.insertAdjacentHTML("beforeend", fallback);
}
Финиш!
Надеемся, вы получили удовольствие (а может и пользу) от этих советов по фронтенду! Современные браузеры дают нам мощные инструменты для создания насыщенных, быстрых и привлекательных интерфейсов, позволяя проявить креативность. Если вы настолько же восхищены открывающимися возможностями, как и мы, вероятно, нам стоит поэкспериментировать вместе.
Автор: m1rko