Анимация размытия

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

ТЛ;ДР

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

Однако этот метод может оказаться весьма неприятным, если применять его без какого-либо переходного периода. Анимация размытия — переход от неразмытого к размытому — кажется разумным выбором, но если вы когда-либо пробовали сделать это в Интернете, вы, вероятно, обнаружили, что анимация совсем не плавная, как показывает эта демонстрация , если у вас нет мощная машина. Можем ли мы добиться большего?

Проблема

Разметка преобразованы в текстуры процессором. Текстуры загружаются в графический процессор. Графический процессор рисует эти текстуры во фреймбуфере с помощью шейдеров. Размытие происходит в шейдер.

На данный момент мы не можем эффективно работать с анимацией размытия. Однако мы можем найти обходной путь, который выглядит достаточно хорошо , но, технически говоря, не является анимированным размытием. Для начала давайте сначала разберемся, почему анимированное размытие происходит медленно. Чтобы размыть элементы в Интернете, существует два метода: свойство filter CSS и фильтры SVG. Благодаря расширенной поддержке и простоте использования обычно используются фильтры CSS. К сожалению, если вам необходима поддержка Internet Explorer, у вас нет другого выбора, кроме как использовать фильтры SVG, поскольку IE 10 и 11 поддерживают их, но не фильтры CSS. Хорошей новостью является то, что наш обходной путь анимации размытия работает с обоими методами. Итак, давайте попробуем найти узкое место, взглянув на DevTools.

Если вы включите «Paint Flashing» в DevTools, вы вообще не увидите никаких вспышек. Похоже, никаких перекрасок не происходит. И это технически правильно, поскольку «перерисовка» означает, что процессору приходится перерисовывать текстуру продвигаемого элемента. Всякий раз, когда элемент одновременно продвигается и размывается, размытие применяется графическим процессором с помощью шейдера.

И фильтры SVG, и фильтры CSS используют фильтры свертки для применения размытия. Фильтры свертки довольно дороги, поскольку для каждого выходного пикселя необходимо учитывать несколько входных пикселей. Чем больше изображение или больше радиус размытия, тем дороже обходится эффект.

И в этом-то и заключается проблема: мы выполняем довольно дорогостоящую операцию графического процессора в каждом кадре, расходуя бюджет кадра в 16 мс и, следовательно, в конечном итоге оказываясь значительно ниже 60 кадров в секунду.

Вниз по кроличьей норе

Что же мы можем сделать, чтобы все прошло гладко? Мы можем использовать ловкость рук! Вместо анимации фактического значения размытия (радиуса размытия) мы предварительно вычисляем пару размытых копий, где значение размытия увеличивается экспоненциально, а затем плавно переходим между ними с помощью opacity .

Перекрестное затухание представляет собой серию перекрывающихся постепенного появления и затухания непрозрачности. Например, если у нас есть четыре этапа размытия, мы постепенно затемняем первый этап и одновременно плавно переходим на второй этап. Как только второй этап достигнет непрозрачности 100%, а первый — 0%, мы затемним второй этап, одновременно плавно исчезая третий этап. Как только это будет сделано, мы, наконец, затухаем третий этап и плавно переходим в четвертую и последнюю версию. В этом сценарии каждый этап займет ¼ от общей желаемой продолжительности. Визуально это очень похоже на настоящее анимированное размытие.

В наших экспериментах экспоненциальное увеличение радиуса размытия за этап дало наилучшие визуальные результаты. Пример: если у нас есть четыре этапа размытия, мы бы применили filter: blur(2^n) к каждому этапу, т.е. этап 0: 1 пиксель, этап 1: 2 пикселя, этап 2: 4 пикселя и этап 3: 8 пикселей. Если мы принудительно разместим каждую из этих размытых копий на отдельном слое (так называемое «продвижение») с помощью will-change: transform , изменение непрозрачности этих элементов должно быть супер-пупер быстрым. Теоретически это позволило бы нам взять на себя дорогостоящую работу по размытию. Оказывается, логика ошибочна. Если вы запустите эту демонстрацию , вы увидите, что частота кадров по-прежнему ниже 60 кадров в секунду, а размытие на самом деле хуже , чем раньше.

Инструменты разработчика   показывает трассировку, в которой графический процессор имеет длительные периоды занятости.

Беглый просмотр DevTools показывает, что графический процессор по-прежнему чрезвычайно загружен и растягивает каждый кадр примерно на 90 мс. Но почему? Мы больше не меняем значение размытия, а только непрозрачность, так что же происходит? Проблема снова заключается в природе эффекта размытия: как объяснялось ранее, если элемент одновременно и продвигается, и размывается, эффект применяется графическим процессором. Таким образом, даже несмотря на то, что мы больше не анимируем значение размытия, сама текстура по-прежнему не размыта, и графический процессор требует повторного размытия в каждом кадре. Причина того, что частота кадров стала еще хуже, чем раньше, связана с тем, что по сравнению с наивной реализацией у графического процессора фактически больше работы, чем раньше, поскольку большую часть времени видны две текстуры, которые необходимо размыть независимо.

То, что мы придумали, некрасиво, но оно делает анимацию невероятно быстрой. Мы возвращаемся к тому, чтобы не продвигать элемент, который будет размыт, а вместо этого продвигаем родительскую оболочку. Если элемент одновременно размыт и повышен, эффект применяется графическим процессором. Это то, что сделало нашу демо-версию медленной. Если элемент размыт, но не повышен, вместо этого размытие растрируется до ближайшей родительской текстуры. В нашем случае это продвинутый родительский элемент-оболочка. Размытое изображение теперь является текстурой родительского элемента и может быть повторно использовано для всех будущих кадров. Это работает только потому, что мы знаем, что размытые элементы не анимируются, и их кэширование действительно полезно. Вот демо , в котором реализована эта техника. Интересно, что думает о таком подходе Moto G4? Спойлер: он считает, что это здорово:

Инструменты разработчика   показывает след, где графический процессор много времени простоит.

Теперь у нас есть большой запас мощности для графического процессора и плавная скорость 60 кадров в секунду. Мы сделали это!

Производство

В нашей демонстрации мы дублировали структуру DOM несколько раз, чтобы копии контента размывались с разной степенью размытия. Вам может быть интересно, как это будет работать в производственной среде, поскольку это может иметь некоторые непредвиденные побочные эффекты со стилями CSS автора или даже с его JavaScript. Ты прав. Введите Shadow DOM!

Хотя большинство людей думают о Shadow DOM как о способе присоединения «внутренних» элементов к своим пользовательским элементам , это также примитив изоляции и повышения производительности! JavaScript и CSS не могут преодолеть границы Shadow DOM, что позволяет нам дублировать контент, не вмешиваясь в стили разработчика или логику приложения. У нас уже есть элемент <div> для каждой копии для растеризации, и теперь мы используем эти <div> в качестве теневых хостов. Мы создаем ShadowRoot с помощью attachShadow({mode: 'closed'}) и прикрепляем копию содержимого к ShadowRoot вместо самого <div> . Мы также должны обязательно скопировать все таблицы стилей в ShadowRoot чтобы гарантировать, что наши копии имеют тот же стиль, что и оригинал.

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

И вот. Изучив конвейер рендеринга Chrome, мы поняли, как можно эффективно анимировать размытие в разных браузерах!

Заключение

К такому эффекту нельзя относиться легкомысленно. Благодаря тому, что мы копируем элементы DOM и помещаем их на отдельный уровень, мы можем расширить возможности устройств более низкого уровня. Копирование всех таблиц стилей в каждый ShadowRoot также представляет собой потенциальный риск для производительности, поэтому вам следует решить, предпочитаете ли вы настроить свою логику и стили, чтобы на них не влияли копии в LightDOM , или использовать нашу технику ShadowDOM . Но иногда наша техника может оказаться стоящей инвестицией. Взгляните на код в нашем репозитории GitHub, а также на демо-версию и напишите мне в Твиттере, если у вас возникнут вопросы!