Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Нравится вам это или нет, но параллакс остается. При разумном использовании он может придавать глубину и изящество веб-приложению. Проблема, однако, заключается в том, что эффективно реализовать параллакс не всегда удается. В этой статье мы рассмотрим решение, которое является одновременно эффективным и, что не менее важно, кроссбраузерным.
Коротко
Не используйте для создания параллакс-анимации прокрутку событий или background-position.
Для создания более четкого параллакс-эффекта используйте 3D-трансформации средствами CSS.
Для Mobile Safari используйте position: sticky, чтобы обеспечить распространение эффекта параллакса.
Если вам нужно готовое решение, заходите в репозиторий UI Element Samples на GitHub и берите Parallax JS хелпер! В репозитории GitHub вы можете увидеть демонстрацию параллакс-прокрутки в реальном времени.
Проблемы с параллаксами
Для начала давайте рассмотрим два распространенных способа достижения эффекта параллакса и, в частности, почему они не подходят для наших целей.
Плохой вариант: использование событий при прокрутке
Ключевым требованием параллакса является то, что он должен быть связан со скроллингом; при каждом изменении положения прокрутки страницы позиция параллакс-элемента должна обновляться. Звучит просто, но важным механизмом современных браузеров является их способность работать асинхронно. В нашем конкретном случае это относится к событиям прокрутки. В большинстве браузеров события прокрутки передаются по принципу "best-effort" ("наилучшая попытка") и не гарантируется, что они будут выполнены для каждого кадра анимации скроллинга!
Эта важная деталь информирует нас о том, почему мы должны избегать JavaScript-решений, перемещающих элементы на основе событий прокрутки: JavaScript не гарантирует, что параллакс будет соответствовать положению прокручиваемой страницы. В ранних версиях Mobile Safari события прокрутки доставлялись фактически в конце скроллинга, что делало невозможным создание скролл-эффекта на основе JavaScript. Более поздние версии действительно передают события прокрутки во время анимации, но, как и в Chrome, по принципу - "наилучшая попытка". Если основной поток занят какой-либо другой работой, события прокрутки сразу не будут доставлены, а значит, эффект параллакса будет потерян.
Плохой вариант: обновление background-position
Еще одна ситуация, которой мы хотели бы избежать, — это прорисовка на каждом кадре. Многие решения пытаются изменить background-position для обеспечения параллакс-вида, что заставляет браузер перерисовывать затрагиваемые части страницы при прокрутке, а это может быть достаточно затратным, что значительно ухудшает анимацию.
Если мы хотим воплотить в жизнь идею создания параллакс-движения, то необходимо что-то, применяемое в качестве ускоренного свойства (что сегодня означает придерживаться трансформаций и непрозрачности), и которое не зависит от событий прокрутки.
CSS в 3D
Scott Kellum и Keith Clark проделали значительную работу в области использования CSS 3D для обеспечения параллакс-движения, и техника, которую они применяют, заключается в следующем:
Настройте содержащийся элемент на прокрутку с
overflow-y: scroll
(и, возможно,overflow-x: hidden
).К этому же элементу примените значение
perspective
, а дляperspective-origin
установите значениеtop left
или0 0
.К его дочерним элементам примените сдвиг в Z и масштабируйте их обратно, чтобы обеспечить параллакс-движение без изменения их размера на экране.
CSS для этого подхода выглядит следующим образом:
.container {
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: scroll;
perspective: 1px;
perspective-origin: 0 0;
}
.parallax-child {
transform-origin: 0 0;
transform: translateZ(-2px) scale(3);
}
Что предполагает наличие HTML-сниппета, подобного этому:
<div class="container">
<div class="parallax-child"></div>
</div>
Настройка масштаба для перспективы
Отодвигая дочерний элемент назад, вы уменьшите его размер пропорционально значению перспективы. Можно рассчитать, насколько его нужно масштабировать, с помощью следующего уравнения: (перспектива — расстояние) / перспектива. Поскольку мы, скорее всего, хотим, чтобы параллакс-элемент отображался с эффектом параллакса, но в том размере, в котором мы его создали, его нужно было бы масштабировать таким образом, а не оставлять как есть.
В приведенном выше коде перспектива равна 1px, а расстояние по Z у parallax-child
равно -2px. Это означает, что элемент должен быть увеличен в 3 pаза, что видно из значения, введенного в код: scale(3)
.
Для любого содержимого, к которому не применяется значение translateZ
, можно подставить нулевое значение. Это означает, что масштаб равен (переспектива — 0) / перспектива, и в итоге дает значение 1, то есть масштаб не увеличивается и не уменьшается. Достаточно удобно, на самом деле.
Как работает этот подход
Важно понять, почему это работает, поскольку мы собираемся использовать эти знания в ближайшее время. Прокрутка — это фактически преобразование, поэтому ее можно ускорить; в основном она включает в себя перемещение слоев с помощью GPU. В обычном скролле, который не имеет понятия перспективы, прокрутка происходит в пропорции 1:1 при сравнении прокручиваемого и его дочерних элементов. Если вы прокручиваете элемент вниз на 300px, то его дочерние элементы трансформируются вверх на ту же величину: 300px.
Однако применение значения перспективы к прокручиваемому элементу нарушает этот процесс; оно изменяет матрицы, лежащие в основе трансформации прокрутки. Теперь прокрутка на 300px может переместить дочерние элементы только на 150px, в зависимости от выбранных значений perspective
и translateZ
. Если у элемента значение translateZ
равно 0, он будет прокручиваться со скоростью 1:1 (как и раньше), но дочерний элемент, сдвинутый по Z от начала перспективы, будет прокручиваться с другой скоростью! В итоге: параллакс-движение. И, что очень важно, это обрабатывается как часть внутреннего механизма прокрутки браузера автоматически, то есть нет необходимости слушать scroll события или изменять background-position
.
Ложка дегтя в бочке меда: Mobile Safari
У каждого эффекта есть свои оговорки, и одна из них касается трансформаций — сохранение 3D-эффектов для дочерних элементов. Если в иерархии между элементом с перспективой и его дочерними элементами с параллаксом существуют еще какие-нибудь элементы, 3D перспектива "сплющивается", то есть эффект теряется.
<div class="container">
<div class="parallax-container">
<div class="parallax-child"></div>
</div>
</div>
В приведенном выше HTML, .parallax-container
будет новым, он фактически сплющит значение perspective
, и мы потеряем эффект параллакса. Решение, в большинстве случаев, довольно простое: добавьте transform-style: preserve-3d
к элементу, что заставит его распространять любые 3D-эффекты (например, наше значение перспективы), которые были применены дальше по дереву.
.parallax-container {
transform-style: preserve-3d;
}
Однако в случае с Mobile Safari все немного сложнее. Применение overflow-y: scroll
к элементу-контейнеру технически работает, но ценой отсутствия способности прокрутки скролл-элемента. Решением является добавление -webkit-overflow-scrolling: touch
, но это также приведёт к сплющиванию perspective
, и мы не получим никакого параллакса.
С точки зрения прогрессивного улучшения, это, вероятно, не слишком большая проблема. Даже если мы не можем использовать параллакс в любой ситуации, наше приложение все равно будет работать, но было бы неплохо найти обходной путь.
position: sticky на помощь!
На самом деле, есть некоторая помощь в виде position: sticky
, которая существует, чтобы позволить элементам "прилипать" к верхней части области просмотра или заданному родительскому элементу во время прокрутки. Спецификация, как и большинство других, довольно объемная, но в ней есть одна жемчужинка:
Липко позиционированный блок размещается аналогично относительно позиционированному блоку, но смещение рассчитывается применительно к ближайшему предку с прокручиваемым блоком или к области просмотра, если у предка нет прокручиваемого блока. — Модуль позиционированного макета CSS Уровень 3
На первый взгляд это может показаться не очень важным, но ключевым моментом в этом предложении является указание на то, как именно рассчитывается прилипание элемента: "смещение вычисляется по отношению к ближайшему предку с прокручиваемым полем". Другими словами, расстояние, на которое нужно переместить прилипший элемент (чтобы он оказался прикрепленным к другому элементу или к области просмотра), рассчитывается до применения любых других преобразований, а не после. Это означает, что, как и в предыдущем примере с прокруткой, если смещение было рассчитано на 300px, появляется новая возможность использовать перспективы (или любое другое преобразование) для манипулирования значением смещения на 300px до того, как оно будет применено к любым прилипшим элементам.
Применяя position: -webkit-sticky
к параллакс-элементу, мы можем эффективно "отменить" эффект сплющивания от -webkit-overflow-scrolling: touch
. Это гарантирует, что параллакс-элемент ссылается на ближайшего предка с прокручиваемым полем, которым в данном случае является .container
. Затем, как и раньше, к .parallax-container
применяется значение perspective, которое изменяет рассчитанное смещение прокрутки и создает эффект параллакса.
<div class="container">
<div class="parallax-container">
<div class="parallax-child"></div>
</div>
</div>
.container {
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
}
.parallax-container {
perspective: 1px;
}
.parallax-child {
position: -webkit-sticky;
top: 0px;
transform: translate(-2px) scale(3);
}
Это позволяет восстановить эффект параллакса для Mobile Safari, что является отличной новостью для всех!
Предостережения по позиционированию "липких" элементов
Однако здесь есть разница: position: sticky
действительно изменяет механику параллакса. Липкое позиционирование пытается прикрепить элемент к прокручиваемому контейнеру, в то время как нелипкая версия этого не делает. Это означает, что параллакс с липким позиционированием становится обратным параллаксу без него:
С
position: sticky
, чем ближе элемент к z=0, тем меньше он перемещается.Без
position: sticky
, чем ближе элемент к z=0, тем больше он движется.
Если все это представляется немного абстрактным, посмотрите на демонстрацию от Robert Flack, которая показывает, как по-разному ведут себя элементы с липким (sticky) позиционированием и без него. Чтобы увидеть разницу, вам понадобится Chrome Canary (на момент написания статьи это версия 56) или Safari.
Это параллакс с использованием перспективы на элементе переполнения (overflow). Начало перспективы находится за пределами скроллера. Это означает, что для того, чтобы элемент параллакса не двигался, он должен находиться бесконечно далеко.
начало перспективы (элемент прокрутки)
Это параллакс с использованием позиционного прилипания. Начало перспективы находится внутри скроллера, поэтому он перемещается вверх по мере прокрутки.
Чтобы элемент параллакса двигался в пропорции 1:1 с прокручиваемым содержимым, он должен быть бесконечно далеко.
Демонстрация от Robert Flack, показывающая, как position: sticky
влияет на параллакс-прокрутку.
Различные ошибки и обходные пути
Однако, как и везде, здесь все еще есть неровности, которые нужно сгладить:
Поддержка технологии прилипания непостоянна. В Chrome она все еще реализуется, в Edge отсутствует полностью, а в Firefox есть ошибки рисования, когда липкость сочетается с трансформациями перспективы. В таких случаях стоит внести небольшой код, чтобы добавлять
position: sticky
(версия с префиксом-webkit-
) при необходимости, и это только для Mobile Safari.Этот эффект не "просто работает" в Edge. Edge пытается обрабатывать прокрутку на уровне ОС, что в целом хорошо, но в данном случае это не позволяет ему обнаружить изменения перспективы при прокрутке. Чтобы исправить это, можно добавить элемент с фиксированной позицией, поскольку это, похоже, переключает Edge на метод прокрутки не на уровне ОС и гарантирует, что он учитывает изменения перспективы.
"Содержимое страницы только что стало огромным!". Многие браузеры учитывают масштаб при определении размера содержимого страницы, но, к сожалению, Chrome и Safari не учитывают перспективу. Поэтому если к элементу применен масштаб, скажем, 3x, вы вполне можете увидеть полосы прокрутки и тому подобное, даже если после применения
perspective
элемент будет иметь размер 1x. Эту проблему можно обойти, масштабируя элементы из правого нижнего угла (с помощьюtransform-origin: bottom right
), и это работает, поскольку приводит к тому, что элементы больших размеров вырастают в "отрицательную область" (обычно левую верхнюю) прокручиваемой области; прокручиваемые области никогда не позволяют видеть или прокручивать содержимое в отрицательной области.
Заключение
Параллакс — интересный эффект в случае продуманного использования. Как вы видите, его можно реализовать таким образом, чтобы он был высокопроизводительным, связанным с прокруткой и кроссбраузерным. Поскольку для получения желаемого эффекта потребуется немного математических выкладок и небольшое количество бойлерплейта, мы подготовили небольшую библиотеку-хелпер и пример, которые вы можете найти в нашем GitHub-репозитории UI Element Samples.
Всех желающих приглашаем на открытое занятие «Анимация модального окна сайта: основы и полезные фишки». На вебинаре рассмотрим один из самых популярных элементов — открытие модального окна при нажатии на кнопку. Также потренируемся в анимации его открытия и закрытия, чтобы сделать элемент отличным от большинства сайтов. Регистрация — по ссылке.