Glassmorphism and SVG

Hi all. I'm Andrey Osipov, front-end developer from Kontur. Almost three years ago, when the company still had the old corporate identity, we were faced with the problem of exporting images in SVG format from Figma. The difficulty was with images where there was a glassmorphism effect, also known as the frosted glass effect.

Recently, out of curiosity, I decided to check if Figma had fixed this problem. The problem remains. There are still no plugins, and Google does not offer good solutions. Therefore, I decided to post this article here, maybe someone will find it useful.

The fact is that with glassmorphism, any element of the illustration is made transparent, and the part of the image behind this element is blurred. And this is what happens when you export to SVG:

The effect simply disappears. Agree, something is not the same without him.

At the same time, export to raster formats (PNG, JPEG) works correctly. I won’t describe here all the advantages of using svg over raster formats, I can only say that in my opinion there are enough of these advantages to get confused and try to understand the problem.

Looking for a sore spot

I was sure that the blur effect in SVG existed and worked, since I had seen images with this effect. Let's figure it out. For example, I took an image from an old brand book with a woman holding a glass software window.

In Figma, this effect is done simply by applying the Background blur effect.

However, after exporting to SVG it looks something like this.

SVG is convenient because it is an XML-based text format, which means it is not so difficult for a passionate front-end developer to understand. Let's try to look inside and see what happened there. (cm ill-01-people-prize.svg)

Inside there are many commands that describe the shape and display of illustration elements. This image uses many tags <path>which allow you to draw an arbitrary shape using various methods, usually Bezier curves. There may also be separate commands that describe primitive forms. In this example, at the very beginning we use <rect>to specify a rectangular background with a gray background. Forms can be combined into groups using the tag <g>.

However, we are more interested in the special section <defs>where commands are written that do not display specific elements, but serve to describe filters, masks and who knows what else. They can be applied via attributes to form and group elements.

Let's try to find the effect we need. The browser's dev tools will help us find the required element:

The glass element is wrapped in a group that has a filter applied to it filter1_bd_4171_33259.

Filter commands are applied sequentially and can be accepted as input using the attribute in various options for depicting an element thanks to keywords. The intermediate result can be saved using the attribute result and can also be passed to an attribute in the following filters. If the command is not explicitly specified init takes the result of the previous filter as input. Let's now look at this filter.

<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>

Everything seems to be clear and should work. But after dancing with tambourines around these filters, you can notice that the attribute does not work in="BackgroundImage"while it is described in specifications. It also says that you need to attach the enable-background attribute to the element for this filter to work. However, even in the examples from the specification it does not work. I tried all sorts of different things, but I didn’t achieve any results either. It turns out that browsers for some reason do not support this feature, which means we cannot use the background in filters.

Essentially in="BackgroundImage" takes a copy of the background part of the image. And we can do the same thing by simply copying all the elements under the glass. Combine them into a group, apply a blur to them and crop them to the shape of the glass. However, copying so many elements manually seems somehow irrational.

We cut with glass

I confess, it helped me find a further solution stack overflow.

Tag <use href="#target"> allows us to copy elements by reference, including groups. If we combine all the elements under the glass into a group, we can copy the background. Group everything that was before the glass into a layer backlayercopy it and apply a simple filter with blur to it.

<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>

Next, you need to trim the background to the shape of the glass. The attribute will help with this clip-path. He needs to pass a reference to the element <clipPath>which should contain the required form. Let's create such an element, and copy the shape of the glass inside using the same <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>

The final SVG turned out like this (cm ill-01-people-prize-result1.svg)

Perfectionism

Great, but there's still a problem. This picture had an opaque gray background. And this is what will happen if you remove it and place the illustration in a place with an arbitrary background.

In most cases, you can stop here and forget about this case, since most often the illustrations have a predictable static background, which can be hardcoded into the SVG itself and everything will work. However, out of curiosity, I went further.

I will immediately warn you that if you place an illustration in a place with an arbitrary non-uniform background, it is impossible to ensure that this background is blurred under glass, and this will work in the same way as with PNG.

In the illustration above with a green background, the problem is that the blurry copy of the background shows through the original background and they overlap each other. The solution here is to cut out a glass-shaped rectangle from the original background.

Use clipPath in this case it won’t work, because it only allows you to crop the image to a given shape. But we need to cut it out the other way around. Therefore, in this case, another feature can help us – masks. The mask is the same as clipPath is specified in the section <defs>and must contain a monochrome image, where the darker the tone, the more transparent the part of the image to which the mask is applied will be. The mask for our case will contain a white background and a black glass shape.

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

In this form, the mask will not work, since our glass has #glass there is already a fill and redefine it in <use> it won't work. This can be solved by transferring the original shape of the shelf to the section <defs> and from there use it in the right place via <use>applying fill and filter. In this case, the fill is applied correctly.

...
<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>

Next we need to apply a mask to the background. If you stupidly apply it to a group #backlayerthen a hole will be cut. But in a place where we use a blurred background, this is not necessary. So you can wrap #backlayer into another group and apply a mask to it.

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

Great, now this SVG works as well as the exported PNG. (cm. ill-01-people-prize-result2.svg)

Unfortunately, this approach does not work very stably. And if you scale the image in every possible way or if the glass has some complex shape, the background of the page may begin to show through its borders. Maybe there is some solution to this, but at that moment I was tired of fighting and didn’t come up with anything.

Addition

Tag <use href="#target" /> It seems that it may not work fully everywhere, since it relates to the SVG 2 specification. In my case, I was faced with the fact that such SVG with the href argument cannot be opened by WebStorm. To fix this, we need a fallback to the old specification <use href="#target" xlink:href="#target" />. And for it to work, you need to add a link to the specification in the root element <svg ... xmlns:xlink="http://www.w3.org/1999/xlink">

Conclusions

It is possible to fix glassmorphism in illustrations after exporting a figma, and I recommend trying this in places where you would like to preserve the SVG goodies. If you get the hang of it, it's done quite quickly. However, if you want to maintain transparency in your illustrations, it may still work poorly in places with this effect. Ideally, I would like to Figma export repaired or automate using some plugin.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *