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

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

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

ТЛ;ДР

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

Если вам нужно готовое решение, перейдите в репозиторий UI Element Samples GitHub и возьмите JS-помощник Parallax ! Вы можете увидеть живую демонстрацию скроллера параллакса в репозитории 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 .

Ложка дегтя: мобильное Safari

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

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

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

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

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

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

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

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

Заключение

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

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