- PVSM.RU - https://www.pvsm.ru -
Мы недавно выпустили новую и улучшенную версию Connect [1], нашего набора инструментов для платформ и магазинов. Группа дизайна Stripe много работала для создания уникальных посадочных страничек [2], которые рассказывают историю [3] для наших основных продуктов. К релизу мы подготовили посадочную страничку Connect, чтобы отразить эти замысловатые, передовые возможности, но в то же время не утратив ясности и простоты изложения.
В этой статье мы опишем, как использовали несколько веб-технологий следующего поколения, чтобы запустить Connect, и пройдёмся по некоторым мелким техническим деталям нашего фронтенд-путешествия.
Ранее в этом году три основных браузера (Firefox, Chrome и Safari) почти одновременно выкатили свои реализации нового модуля CSS Grid Layout [4]. Эти спецификации дают разработчикам двухмерную макетную систему, которая проста в использовании и невероятно мощная. Посадочная страница [1] Connect полагается на сетки CSS практически везде, что делает некоторые на первый взгляд хитрые дизайнерские решения банально простыми в реализации. В качестве примера, давайте спрячем содержимое заголовка и сфокусируемся на фоне:
Исторически, мы создавали такие фоновые полосы (страйпы) с помощью абсолютного позиционирования, чтобы точно разместить каждый страйп на странице. Этот способ работает, но хрупкое позиционирование часто ведёт к небольшим проблемкам: например, из-за ошибок округления между полосами может образоваться зазор 1 px. Таблицы стилей к тому же быстро разбухают, и их труднее поддерживать, поскольку медийные запросы усложняются для учёта различий фона на разных размерах экрана.
CSS Grid устраняет практически все эти проблемы. Мы просто определяем гибкую сетку и размещаем страйпы в соответствующие ячейки. В Firefox есть удобный инспектор сеток [5], который визуализирует структуру вашего макета. Посмотрим, как он выглядит:
Мы выделили три страйпа и убрали эффект наклона для наглядности. Вот как будет выглядеть соответствующий код 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 [6], а значит, можете здесь тоже использовать знакомые свойства, которые так любите, такие как justify-content
и align-items
.
Заголовок целевой страницы показывает несколько кубов как визуальную метаформу строительных блоков, которые составляют Connect. Эти летающие кубы вращаются в 3D на случайных скоростях (в определённом диапазоне) и освещаются одним источником света, который динамически подсвечивает соответствующие поверхности, видео: cubes.mp4 [7]
Эти кубы — простые элементы 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
для вычисления и обновления каждой стороны в любой точке вращения. В каждом кадре нужно определить три вещи:
Есть и другие соображения, которые нужно принять в расчёт (например, улучшение производительности с помощью requestIdleCallback
на JavaScript и backface-visibility
на CSS), но это главные основы для логики анимации.
Мы можем вычислить видимость и трансформацию для каждой стороны, непрерывно отслеживая их состояния и обновляя их с помощью простых математических операций. При использовании чистых функций [8] и фич ES2015, таких как шаблонные литералы [9], всё становится даже проще. Вот два коротких фрагмента кода 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-хелперов и других элементарных абстракций. Последняя достойная упоминания деталь — это техника, которая делает анимацию менее навязчивой, в зависимости от настроек пользователя: видео [10].
[10]
Нажмите для просмотра видео
На 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);
};
По всему сайту мы используем кастомные трёхмерные компьютерные устройства как витрину для клиентов 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 как эксклюзивного формата представления документов, но настолько же легко выйти за рамки и излишне использовать его визуальные возможности. Неважно, какую технологию вы выбрали, оптимизируйте её для пользователя! Значит, уделяйте пристальное внимание производительности на стороне клиента, доступности и опциям отката для старых браузеров.
[11]В разделе Onboarding & Verification [12] представлено демо Express, новой системы адаптации начинающих пользователей Connect. Вся анимация целиком построена на программном коде и в основном полагается на новые Web Animations API [13].
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 и независимые функции трансформации. Если вы не уверены, какую технологию выбрать для своих анимаций, то вероятно, варианты можно расставить в следующем приоритетном порядке:
hover
.@keyframes
, а часто и явного animation-fill-mode
. (А именованные штуки всегда были сложнейшими частями компьютерной науки!)Вне зависимости от того, какую технику вы используете, вот несколько простых советов, которые вы всегда можете использовать, чтобы ваша анимация выглядела значительно лучше:
timing-function
вроде ease-in
, ease-out
и linear
. Вы сэкономите много времени, если глобально определите количество кастомных переменных cubic-bezier [14].transform
и opacity
) и сбрасывать анимации на GPU по возможности (применяя will-change
).Воспроизведение анимации Express начинается автоматически как только она появляется в поле видимости (вы можете испытать это, прокручивая страничку [1]). Обычно это сопровождается наблюдением за скроллингом, который срабатывает как триггер, но исторически это реализовали через ресурсоёмкие слушатели событий, что приводило к многословному и неэффективному коду.
Целевая страница Connect использует новый Intersection Observer API [16], который обеспечивает намного, намного более надёжный и производительный способ определять видимость элемента. Вот как мы начинаем воспроизводить анимацию 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);
}
Надеемся, вы получили удовольствие (а может и пользу) от этих советов по фронтенду! Современные браузеры дают нам мощные инструменты для создания насыщенных, быстрых и привлекательных интерфейсов, позволяя проявить креативность. Если вы настолько же восхищены открывающимися возможностями, как и мы, вероятно, нам стоит поэкспериментировать вместе [17].
Автор: m1rko
Источник [18]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/258681
Ссылки в тексте:
[1] Connect: https://stripe.com/connect
[2] уникальных посадочных страничек: https://stripe.com/atlas
[3] рассказывают историю: https://stripe.com/sigma
[4] CSS Grid Layout: https://www.w3.org/TR/css-grid-1/
[5] инспектор сеток: https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Examine_grid_layouts
[6] Box Alignment: https://www.w3.org/TR/css-align-3/
[7] cubes.mp4: https://stripe-images.s3.amazonaws.com/blog/connect-frontend/cubes.mp4
[8] чистых функций: https://alistapart.com/article/making-your-javascript-pure
[9] шаблонные литералы: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
[10] видео: https://stripe-images.s3.amazonaws.com/blog/connect-frontend/reduced-motion.mp4
[11] Image: https://stripe-images.s3.amazonaws.com/videos/connect/express.mp4
[12] Onboarding & Verification: https://stripe.com/connect#onboarding-verification
[13] Web Animations API: https://www.w3.org/TR/web-animations-1/
[14] кастомных переменных cubic-bezier: http://tinyurl.com/css-easings
[15] подтормаживаний: http://jankfree.org/
[16] Intersection Observer API: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
[17] поэкспериментировать вместе: https://stripe.com/jobs?ref=blog#design
[18] Источник: https://habrahabr.ru/post/331548/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.