Эффективный параллакс

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

Иллюстрация параллакса.

TL;DR

  • Не используйте события прокрутки или background-position для создания параллакс-анимаций.
  • Используйте 3D-преобразования CSS для создания более точного эффекта параллакса.
  • Для мобильного Safari используйте position: sticky , чтобы обеспечить распространение эффекта параллакса.

Если вам нужно готовое решение, перейдите в репозиторий UI Element Samples на GitHub и скачайте Parallax Helper JS ! Вы можете посмотреть живую демонстрацию скроллера с эффектом параллакса в репозитории на GitHub.

Проблемные параллаксеры

Для начала давайте рассмотрим два распространенных способа достижения эффекта параллакса и, в частности, почему они не подходят для наших целей.

Плохо: использование событий прокрутки

Ключевое требование параллакса — его связь с прокруткой: при каждом изменении положения прокрутки страницы позиция параллаксирующего элемента должна обновляться. Хотя это звучит просто, важным механизмом современных браузеров является их способность работать асинхронно. В нашем конкретном случае это относится к событиям прокрутки. В большинстве браузеров события прокрутки доставляются по принципу «максимальных усилий» и не гарантируются в каждом кадре анимации прокрутки!

Эта важная информация объясняет, почему следует избегать решений на основе JavaScript, которые перемещают элементы в зависимости от событий прокрутки: JavaScript не гарантирует, что параллакс будет соответствовать положению прокрутки страницы . В старых версиях Mobile Safari события прокрутки фактически доставлялись в конце прокрутки, что делало невозможным создание эффекта прокрутки на основе JavaScript. Более поздние версии действительно доставляют события прокрутки во время анимации, но, как и Chrome, по принципу «максимальных усилий». Если основной поток занят другой работой, события прокрутки не будут доставлены немедленно, а значит, эффект параллакса будет потерян.

Плохо: обновление background-position

Ещё одна ситуация, которой хотелось бы избежать, — это отрисовка в каждом кадре. Многие решения пытаются изменить background-position для создания эффекта параллакса, что заставляет браузер перерисовывать соответствующие области страницы при прокрутке, что может быть достаточно затратно, чтобы существенно испортить анимацию.

Если мы хотим реализовать обещание параллакс-движения, нам нужно что-то, что можно применять как ускоренное свойство (что сегодня означает придерживаться преобразований и непрозрачности) и что не зависит от событий прокрутки.

CSS в 3D

Скотт Келлум и Кит Кларк проделали значительную работу в области использования 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 раза , что, как вы видите, соответствует значению, подставленному в код: scale(3) .

Для любого контента, к которому не применено значение translateZ , можно указать нулевое значение. Это означает, что масштаб равен (perspective - 0) / perspective , что в сумме даёт значение 1, то есть объект не масштабируется ни вверх, ни вниз. Очень удобно, правда.

Как работает этот подход

Важно понимать, почему это работает, поскольку мы вскоре воспользуемся этими знаниями. Прокрутка, по сути, представляет собой преобразование, поэтому её можно ускорить; в основном она заключается в смещении слоёв с помощью графического процессора. При типичной прокрутке, которая не учитывает перспективу, прокрутка происходит в соотношении 1:1 при сравнении прокручиваемого элемента и его дочерних элементов. Если прокрутить элемент вниз на 300px , то его дочерние элементы будут преобразованы вверх на ту же величину: 300px .

Однако применение значения перспективы к прокручиваемому элементу нарушает этот процесс; оно изменяет матрицы, лежащие в основе преобразования прокрутки. Теперь прокрутка на 300 пикселей может сместить дочерние элементы только на 150 пикселей, в зависимости от выбранных вами значений 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 , которая позволяет элементам «прилипать» к верхней части области просмотра или заданному родительскому элементу во время прокрутки. Спецификация, как и большинство подобных, довольно объёмная, но в ней есть одна полезная вещица:

На первый взгляд это может показаться не таким уж важным, но ключевой момент в этом предложении — это указание на то, как именно рассчитывается липкость элемента: «смещение вычисляется относительно ближайшего предка с прокручиваемым блоком» . Другими словами, расстояние, на которое необходимо переместить липкий элемент (чтобы он выглядел прикреплённым к другому элементу или области просмотра), рассчитывается до применения любых других преобразований, а не после . Это означает, что, как и в предыдущем примере с прокруткой, если смещение было рассчитано как 300 пикселей, появляется новая возможность использовать перспективы (или любые другие преобразования) для управления этим значением смещения в 300 пикселей до его применения к любым липким элементам.

Применив 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 действительно меняет механику параллакса. Прилипающее позиционирование пытается, по сути, прикрепить элемент к прокручиваемому контейнеру, в то время как вариант без sticky этого не делает. Это означает, что параллакс с sticky в итоге оказывается обратным параллаксу без sticky:

  • При position: sticky чем ближе элемент к z=0, тем меньше он перемещается.
  • Без position: sticky , чем ближе элемент к z=0, тем больше он перемещается.

Если всё это кажется вам немного абстрактным, взгляните на эту демонстрацию Роберта Флэка, которая демонстрирует, как элементы ведут себя по-разному с фиксированным позиционированием и без него. Чтобы увидеть разницу, вам понадобится Chrome Canary (версия 56 на момент написания статьи) или Safari.

Скриншот перспективы параллакса

Демонстрация Роберта Флэка, показывающая, как position: sticky влияет на прокрутку параллакса.

Различные ошибки и способы их устранения

Однако, как и в любом деле, есть еще неровности, которые необходимо сгладить:

  • Поддержка Sticky нестабильна. В Chrome поддержка пока не реализована, Edge её полностью не поддерживает, а в Firefox наблюдаются ошибки отрисовки при сочетании Sticky с перспективными преобразованиями . В таких случаях стоит добавить небольшой код, чтобы position: sticky (версия с префиксом -webkit- ) добавлялась только при необходимости, что актуально только для мобильной версии Safari.
  • Этот эффект не «просто работает» в Edge. Edge пытается обрабатывать прокрутку на уровне ОС, что в целом хорошо, но в данном случае мешает ему отслеживать изменения перспективы во время прокрутки. Чтобы исправить это, можно добавить элемент с фиксированным положением, поскольку это, по-видимому, переключает Edge на метод прокрутки, не зависящий от ОС , и гарантирует, что Edge учитывает изменения перспективы.
  • «Содержимое страницы стало просто огромным!» Многие браузеры учитывают масштаб при определении размера содержимого страницы, но, к сожалению, Chrome и Safari не учитывают перспективу . Поэтому, если к элементу применен, скажем, трёхкратный масштаб, вы можете увидеть полосы прокрутки и тому подобное, даже если после применения perspective элемент имеет масштаб 1x. Эту проблему можно обойти, масштабируя элементы от нижнего правого угла (с помощью transform-origin: bottom right ), что работает, поскольку приводит к тому, что слишком большие элементы попадают в «отрицательную область» (обычно в верхний левый угол) прокручиваемой области; прокручиваемые области никогда не позволяют увидеть или прокрутить содержимое в отрицательной области.

Заключение

Параллакс — интересный эффект при грамотном использовании. Как видите, его можно реализовать производительно, с учётом прокрутки и кроссбраузерно. Поскольку для достижения желаемого эффекта требуется немного математических выкладок и немного шаблонного кода, мы создали небольшую вспомогательную библиотеку и пример, которые можно найти в нашем репозитории UI Element Samples на GitHub .

Поиграйте и расскажите нам, как у вас идут дела.