Всем привет. Я Андрей Осипов, фронтендер из Контура. Почти три года назад, когда у компании был еще старый фирменный стиль, мы столкнулись с проблемой экспорта из фигмы изображений в формате SVG. Сложность была с изображениями, где был эффект глассморфизма, он же эффект матового стекла (frosted glass).

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

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

Эффект просто напросто пропадает. Согласитесь, без него уже как-то что-то не то.

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

Ищем больное место

Я был уверен, что эффект размытия в SVG есть и работает, так как встречал изображения с этим эффектом. Давайте разбираться. Для примера я взял изображение из старого брендбука с женщиной, держащей стеклянное программное окошко.

В фигме этот эффект делается просто применением эффекта Background blur.

Однако после экспорта в SVG оно выглядит примерно так.

SVG удобен тем, что это текстовый формат на основе XML, а значит увлеченному фронтендеру не так сложно с ним разобраться. Давайте попробуем заглянуть внутрь и посмотрим что там случилось. (см ill-01-people-prize.svg)

Внутри находится множество команд, описывающих форму и отображение элементов иллюстрации. В данном изображении используется множество тегов <path>, которые позволяют нарисовать произвольную форму различными методами, как правило кривыми Безье. А также могут встречаться отдельный команды, описывающие примитивные формы. В данном примере в самом начале используется <rect>, чтобы задать прямоугольный фон с серой подложкой. Формы могут объединятся в группы тегом <g>.

Однако нас больше интересует специальная секция <defs>, где записаны команды, которые не отображают конкретные элементы, однако служат для описания фильтров, масок и черт знает чего ещё. Они могут быть применены через атрибуты к элементам форм и групп.

Давайте попробуем отыскать нужный нам эффект. Найти нужный элемент нам помогут dev tools браузера:

Элемент со стекляшкой обернут в группу, к которой применен фильтр filter1_bd_4171_33259.

Команды фильтров применяются последовательно и могут принимать на вход с помощью атрибута in различные варианты изображения элемента благодаря ключевым словам. Промежуточный результат может сохранятся с помощью атрибута result и также может быть передан в атрибут in следующих фильтров. Если у команды явно не указан in, она берет на вход результат предыдущего фильтра. Давайте теперь разберем этот фильтр.

<filter id="filter1_bd_4171_33259" x="213" y="390" width="461" height="325" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
    <desc>
        feFlood заливает область фильтра цветом, 
        но в данном случае он заливает прозрачным
        Я так и не понял для чего нужно это преобразование. 
    </desc>
    <feFlood flood-opacity="0" result="BackgroundImageFix" />
 
    <desc>
        Берем фоновое изображение, блюрим и потом используем альфаканал элемента в качестве маски,
        чтобы обрезать заблюренный фон по форме элемента. Потом сохраняем результат в переменную
        "effect1_backgroundBlur_4171_33259"
    </desc>
    <feGaussianBlur in="BackgroundImage" stdDeviation="12" />   
    <feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_4171_33259" />
 
    <desc>
        Далее ещё несколько хитрых манипуляция, чтобы сделать тень
    </desc>
    <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
    <feOffset dy="16" />
    <feGaussianBlur stdDeviation="24" />
    <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" />
 
    <desc>
        Накладываем размытый фон на тень
    </desc>
    <feBlend mode="normal" in2="effect1_backgroundBlur_4171_33259" result="effect2_dropShadow_4171_33259" />
 
    <desc>
        Накладываем получившийся результат на оригинальный элемент
    </desc>
    <feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_4171_33259" result="shape" />
</filter>

Вроде всё чётко и должно работать. Но после танцев с бубнов вокруг этих фильтров можно заметить, что не работает атрибут in="BackgroundImage", при этом он описан в спецификации. Также там написано, что надо навешивать атрибут enable-background на элемент для работы этого фильтра. Однако даже в примерах из спецификации оно не работает. Я попробовав всяко разно, тоже результата не добился. Получается, браузеры по какой-то причине не поддерживают эту фичу, а значит мы не можем использовать фон в фильтрах.

По сути in="BackgroundImage" берет копию фоновой части изображения. И мы ведь можем сделать тоже самое, просто скопировав все элементы, находящиеся под стеклом. Объединить их в группу, применить к ним размытие и обрезать по форме стекла. Однако копировать столько элементов вручную выглядит как-то не рационально.

Режем стеклом

Каюсь, дальнейшее решение мне помог найти stack overflow.

Тег <use href="#target"> позволяет нам копировать элементы по ссылке, в том числе и группы. Если мы объединим в группу все находящиеся под стеклом элементы, то сможем скопировать фон. Группируем всё, что находилось до стекла, в слой backlayer, копируем и применяем к нему простой фильтр с размытием.

<g id="backlayer">...</g>
<use href="#backlayer" filter="url(#blur)" />
<path d="..." fill="url(#paint0_linear_4171_33259)" filter="url(#filter1_bd_4171_33259)"/>
...
<defs>
    <filter id="blur">
        <feGaussianBlur in="SourceGraphic" stdDeviation="12" result="blurred" />
    </filter>
    ...
</defs>

Далее надо обрезать фон по форме стекла. В этом поможет атрибут clip-path. Ему нужно передать ссылку на элемент <clipPath>, который должен содержать нужную форму. Создадим такой элемент, а внутрь скопируем форму стекла, используя тот же <use href="#target">.

<svg width="780" height="680" viewBox="0 0 780 680" fill="none" xmlns="http://www.w3.org/2000/svg">
    <g id="backlayer">...</g>
    <use href="#backlayer" filter="url(#blur)" clip-path="url(#glassmask)" />
    <path id="glass" d="..." fill="url(#paint0_linear_4171_33259)" filter="url(#filter1_bd_4171_33259)"/>
    ...
    <defs>
        <path id="glass" d="..." fill="url(#paint0_linear_4171_33259)" filter="url(#filter1_bd_4171_33259)"/>
        <clipPath id="glassmask">
            <use href="#glass"/>
        </clipPath>
        <filter id="blur">
            <feGaussianBlur in="SourceGraphic" stdDeviation="12" result="blurred" />
        </filter>
        ...
    </defs>
</svg>

Итоговый SVG получился такой (см ill-01-people-prize-result1.svg)

Перфекционизм

Отлично, но есть ещё проблемка. У этой картинки был непрозрачный серый фон. И вот что будет, если его удалить и поместить иллюстрацию в место с произвольным фоном.

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

Сразу предупрежу, что если вы помещаете иллюстрацию в место с произвольным неоднородным фоном, добиться того, чтобы этот фон размывался под стеклом невозможно, и это будет работать аналогично как и с PNG.

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

Использовать clipPath в этом случае уже не получится, ведь он позволяет только обрезать изображение по заданной форме. А нам надо наоборот вырезать. Поэтому в данном случае нам может помочь другая фича - маски. Маска также как и clipPath задается в секции <defs>и должна содержать монохромное изображение, где чем темнее тон, тем прозрачнее будет часть изображение, к которому применяется маска. Маска для нашего случая будет содержать белый фон и черную форму стекла.

<defs>
        ...
        <mask id="bkgMask">            
            <rect width="780" height="680" fill="#fff" />            
            <use href="#glass" fill="#000"/>
        </mask>
        ...
</defs>

В таком виде маска не заработает, так как у нашего стеклышка #glass уже есть заливка и переопределить её в <use> не получится. Решить это можно тем, что перенести оригинальную форму стелышка в секцию <defs> и оттуда уже использовать в нужном месте через <use>, применяя заливку и фильтр. В таком случае заливка применяется корректно.

...
<use href="#backlayer" filter="url(#blur)" clip-path="url(#glassmask)" />
<use href="#glass" filter="url(#filter1_bd_4171_33259)"  fill="url(#paint0_linear_4171_33259)" />
...
<defs>
        <path id="glass" d="..." />
        <mask id="bkgMask">            
            <rect width="780" height="680" fill="#fff" />            
            <use href="#glass" fill="#000"/>
        </mask>

Далее нам надо применить маску к фону. Если тупо применить её к группе #backlayer, то вырежется дырка. А в месте, где мы используем размытый фон, этого не надо. Поэтому можно обернуть #backlayer в ещё одну группу и применить маску к ней.

<g mask="url(#bkgMask)">
        <g id="backlayer">
            ...
        </g>
 </g>

Отлично, теперь этот SVG работает не хуже экспортированного PNG. (см. ill-01-people-prize-result2.svg)

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

Дополнение

Тег <use href="#target" /> вроде как полноценно может работать не везде, так как относится к спецификации SVG 2. В моем случае, я столкнулся c тем, что такие SVG с аргументом href не может открыть WebStorm. Чтобы это починить, нужен фоллбэк к старой спецификации <use href="#target" xlink:href="#target" />. И чтобы он заработал, нужно добавить ссылку на спецификацию в корневой элемент <svg ... xmlns:xlink="http://www.w3.org/1999/xlink">

Выводы

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

Комментарии (5)


  1. ImagineTables
    25.10.2024 09:35

    Скажите, пожалуйста, а зачем вам это надо? В каком юз-кейсе? Вопрос, само собой, не про дизайн с матовыми стёклами (такой я сам использую), а про то, кто, кроме браузера, должен его отображать.

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

    В HTML'е под такие вещи выделили специальный DSL, чтобы отделить эффекты (backdrop-filter: blur(5px);) от самой разметки. И это очень правильно и здорово, потому что описывать сами фигуры и то, как они отображаются, на одном и том же языке так же ужасно, как задавать цвета и шрифты атрибутами. Тяжёлое наследие XML.

    Так вот, для рендеринга браузером ничто не мешает эту картинку включить в документ, а её части (стекло), стилизовать CSS'ом (эффект размытия). А чем ещё, кроме браузера, её рендерить на клиенте? Где она у вас используется? Тем более, я бы просто не доверил такой сложный SVG (так сказать, с блендингом и эффектами) какому-нибудь SvgControl из типичных GUI-библиотек).


    1. IRaySans Автор
      25.10.2024 09:35

      В моем случае речь про браузер, при отображении иллюстраций на странице я иногда предпочитаю использовать <img>, а не инлайнтить svg.

      backdrop-filter как и многие css свойства не работает с svg элементами, даже если они заинлайнены в html, у них другие свойства.

      описывать сами фигуры и то, как они отображаются, на одном и том же языке так же ужасно

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


      1. ImagineTables
        25.10.2024 09:35

        backdrop-filter как и многие css свойства не работает с svg элементами, даже если они заинлайнены в html, у них другие свойства.

        Я, собственно, потому и спросил, какая реальная задача решается ))

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

        А если мы делаем UI и нам реально нужно что-то снизу отобразить и размывать фон с учётом контекста (допустим, эта скромная девушка скрывает номер счёта и остаток на нём), всегда можно нарезать слои так, чтобы применить нужные свойства к слою целиком. Слои, естественно, при желании можно оставить векторными (SVG).

        зато легко открыть в любом векторном редакторе и подправить, SVG + CSS ты уже так не откроешь

        При описанном выше подходе в каком-нибудь Inkscape открываются отдельные элементы (девушка или окно), а не всё в сборе. Если, конечно, речь идёт про UI (об этом и был мой вопрос): UI ведь всё равно в одну картинку не упихать никак.

        Я вообще делаю целые библиотеки из <defs>'ов, с которыми обращаюсь как с компонентами. Допустим, надо показать поддержку разных групп disabled people, одна — слабовидящие, другая — которым трудно пользоваться мышкой. Первые обозначены векторным изображением уха, вторые — векторным изображением клавиатуры, и оба изображения взяты в сердечко. Итого, у меня три векторных изображения в виде <symbol>, и две пары <use href="">, потому что зачем два раза рисовать сердечко? А если его контур захочется перерисовать? Пример, конечно, упрощённый, но с более сложной графикой это тоже работает и приносит пользу.


        1. IRaySans Автор
          25.10.2024 09:35

          Реальная задача была в том чтобы отобразить модальное окно где есть картинка с одной стороны и текст с предложением купить продукт с другой)

          Картинка ничто не перекрывала и с UI никак не смешивалась, поэтому в backdrop-filter необходимости не было.

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

          При описанном выше подходе в каком-нибудь Inkscape открываются отдельные элементы (девушка или окно), а не всё в сборе.

          Ну это несовершенство редактора) Фигма нормально открывает эту картинку, но всирает опять этот фильтр) В моем случае ещё удобно что WebStorm открывает картинку корректно.


          Целью статьи было вообще рассказать как исправить корявый экспорт фигмы, а уже применять это или нет, действительно зависит от задачи.


  1. Perfecti-ist
    25.10.2024 09:35

    Это самый сложный и бесполезный костыль, который я когда либо видел