В CSS есть хорошее свойство filter, которое поддерживает целых 11 эффектов (насыщенность, яркость, контрастность и др.)
CSS-фильтры достаточно мощные и удобные, но в то же время их возможности ограничены цветовыми манипуляциями и размытием. Применять их в большинстве случаев можно только к изображениям.
Однако у нас есть гораздо более мощный функциональный инструмент, и уже очень давно, хотя вы возможно даже не догадывались об этом, – это SVG. В этой статье мы поговорим именно о SVG-фильтрах, известных также как «примитивы фильтров»: об их возможностях и применении.
Для начала сравним две технологии на примере размытия. В CSS за него отвечает функция blur()
, которая применяет к элементу размытие по Гауссу.
Изображение размывается равномерно в обоих направлениях (по осям X и Y).
На самом деле эта функция – просто упрощенный и ограниченный шорткат для blur-примитива фильтра из SVG, который также позволяет создавать однонаправленный эффект размытия (только по X или только по Y).
Применять SVG-фильтры можно и к SVG-элементам и к обычным элементам HTML с помощью CSS-функции url()
. То есть вы можете создать нужный фильтр в SVG (об этом позже) с идентификатором myAwesomeEffect
и применить его с помощью вот такой конструкции:
.el {
filter: url(#myAwesomeEffect);
}
С помощью SVG-фильтров вы сможете создавать в браузере вполне себе «Photoshop-эффекты»! Пора раскрыть и использовать их потенциал на полную катушку.
Но что насчет браузерной поддержки..?
Поддержка браузерами
Поддержка у SVG-фильтров впечатляющая. Однако конкретный способ применения эффекта может отличаться в разных браузерах и для разных примитивов. Также могут быть различия в отображении фильтров в SVG и HTML-элементами.
Лучше всего использовать эту технологию в качестве Progressive Enhancement (прогрессивного улучшения) поверх привычных безфильтровых конструкций.
Итак, как создать эффект фильтра в SVG?
Элемент <filter>
Как и у градиентов, масок, паттернов и прочих графических эффектов, для фильтров в SVG выделен специальный элемент – <filter>.
Сам по себе он не отображается на странице, поэтому его можно даже не упаковывать в тег <defs>
. Фильтр работает только на конкретном объекте при наличии явной ссылки на него.
Минимальный синтаксис определения и применения фильтра выглядит так:
<svg width="600" height="450" viewBox="0 0 600 450">
<filter id="myFilter">
<!-- здесь описываются нужные примитивы фильтров -->
</filter>
<image xlink:href="image.png"
width="100%" height="100%" x="0" y="0"
filter="url(#myFilter)"></image>
</svg>
Этот кусок кода не будет работать, так как тут нет ни одного реального фильтра. Чтобы что-то увидеть, нужно добавить один или несколько примитивов внутрь тега <filter>
. Другими словами, здесь мы только создали контейнер для будущих фильтров.
Примитивы фильтров
В SVG элемент <filter>
должен содержать набор дочерних примитивов, каждый из которых выполняет одну из основных графических операций над входящим объектом (или входящими объектами).
Каждый примитив удобно называется по той операции, которую он выполняет. Например, для эффекта размытия по Гауссу используется примитив feGaussianBlur. Префикс fe означает filter effect.
Выглядит это примерно так (размытие по Гауссу на 5 пикселей):
<svg width="600" height="450" viewBox="0 0 600 450">
<filter id="myFilter">
<feGaussianBlur stDeviation="5"></feGaussianBlur>
</filter>
<image xlink:href="image.png"
width="100%" height="100%" x="0" y="0"
filter="url(#myFilter)"></image>
</svg>
Сейчас в спецификации определено 17 примитивов, с помощью которых можно создавать очень крутые фильтры, включая генерацию шума, текстуры, световые эффекты, манипуляции с каналами цвета и многое другое.
Каждый фильтр принимает некоторые графические данные на вход и отдает на выходе результат своей работы. При этом вывод одного фильтра может быть использован в качестве входных данных для другого. Это очень важно и очень круто. Благодаря такой технике вы имеет почти бесконечное количество комбинаций различных эффектов.
У каждого примитива может быть один или два входных значения и всегда один-единственный результат. Входные данные указываются в атрибуте in
(и in2
, если необходимо), результат – в атрибуте result
. То есть вы можете дать псевдоним результату одного фильтра и указать его в атрибуте in
другого.
Если у одного примитива в наборе не указан псевдоним для результата, то его выходные данные автоматически станут входными для следующего примитива.
Аналогично, если у примитива не указаны в атрибутах входные данные, то они берутся из результата работы предыдущего фильтра.
Сейчас это выглядит довольно запутано, но как только мы перейдем к примерам, станет понятнее.
В качестве входных данных могут использоваться не только результаты работы предыдущих фильтров. Например, можно передать в атрибут in
(или in2
) следующее:
- SourceGraphic – исходный элемент, к которому изначально применяется весь фильтр, например, изображение;
- SourceAlpha – то же самое с небольшим уточнением: тут имеет значение только альфа-канал элемента. Например, для jpeg-изображения это будет черный прямоугольник размером с само изображение.
Это полезные значения, которые точно вам пригодятся. Зачастую для какого-нибудь фильтра в центре цепочки требуется исходная графика или только ее альфа-канал.
Этот фрагмент кода демонстрирует применение целого набора примитивов фильтров к изображению. Не заморачивайтесь конкретными эффектами. Сейчас вы должны только понять, как определяются и используются входные и выходные данные.
<svg width="600" height="400" viewBox="0 0 850 650">
<filter id="filter">
<feOffset in="SourceAlpha" dx="20" dy="20"></feOffset>
<!-- у предыдущего фильтра нет псевдонима для результата работы,
а у следующего за ним не уточняются входные данные,
поэтому результат первого автоматически подается на вход второму -->
<feGaussianBlur stdDeviation="10" result="DROP"></feGaussianBlur>
<!-- использовать для псевдонимов КАПС -
это хороший способ сделать код более понятным и читаемым -->
<feFlood flood-color="#000" result="COLOR"></feFlood>
<!-- Этот примитив использует выходные данные двух предыдущих -->
<feComposite in="DROP" in2="COLOR" operator="in" result="SHADOW1"></feComposite>
<feComponentTransfer in="SHADOW1" result="SHADOW">
<feFuncA type="table" tableValues="0 0.5"></feFuncA>
</feComponentTransfer>
<!-- можно использовать для входа результаты работы любых примитивов,
независимо от их порядка и расположения в DOM -->
<feMerge>
<feMergeNode in="SHADOW"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<image xlink:href="image.png" x="0" y="0"
width="100%" height="100%" filter="url(#filter)"></image>
</svg>
Прежде чем перейти к реальному примеру, рассмотрим еще одну концепцию – область фильтра (filter region).
Область Фильтра
Важно понимать, на какую область распространяется действие фильтра. Например, у вас есть большой SVG-объект, состоящий из множества элементов, а фильтр вы применяется только к одному элементу (или небольшой группе).
Элементы в SVG имеют «регионы». Каждый регион – это минимальный прямоугольник, ограничивающий элемент (Bounding Box, bbox). Например, для текста этот контейнер выглядит вот так (розовый прямоугольник на рисунке):
Обратите внимание, что этот прямоугольник немного отступает от самого текста, так как он учитывает еще и высоту строки.
Область действия фильтра ограничивается этой самой рамкой. Если применить к этому тексту какой-нибудь фильтр, он будет обрезан по контуру этого прямоугольника. Это разумно, но не очень практично, ведь большинство фильтров выходят за этот контур:
Эффект размытия, применяемый к тексту, обрезается как с правой, так и с левой стороны области ограничивающего прямоугольника текста.Чтобы предотвратить это обрезание, нужно расширить область фильтра, изменив атрибуты x
, y
, width
, height
самого фильтра (элемента <filter>
).
На самом деле по умолчанию так и происходит, дефолтные значения выглядят вот так:
<filter x="-10%" y="-10%" width="120%" height="120%"
filterUnits="objectBoundingBox">
<!-- filter operations here -->
</filter>
При необходимости вы можете их переопределить, увеличив или уменьшив область фильтра.
Единицы измерения в атрибутах x
, y
, width
, height
зависят от значения атрибута filterUnits
, который определяет систему отсчета. Всего их два:
- objectBoundingBox – значение по умолчанию. При этом координаты и размеры рассчитываются как проценты (или доли) от размера bounding box элемента.
- userSpaceOnUse – отсчитывает все значения (обычно в пикселях) относительно системы координат svg-элемента.
<!-- Using objectBoundingBox units -->
<filter id="filter"
x="5%" y="5%" width="100%" height="100%">
<!-- Using userSpaceOnUse units -->
<filter id="filter"
filterUnits="userSpaceOnUse"
x="5px" y="5px" width="500px" height="350px">
Быстрый совет: визуализация текущей области фильтра с помощью feFlood
Если вы захотите визуализировать область действия фильтра, можете использовать примитив feFlood, который заполнит ее цветом, указанным в атрибуте flood-color.
<svg width="600px" height="400px" viewBox="0 0 600 400">
<filter id="flooder" x="0" y="0" width="100%" height="100%">
<feFlood flood-color="#EB0066" flood-opacity=".9"></feFlood>
</filter>
<text dx="100" dy="200" font-size="150" font-weight="bold" filter="url(#flooder)">Effect!</text>
</svg>
feFlood может принимать не только цвет заливки, но и ее прозрачность (flood-opacity
).
Этот фильтр зальет всю свою область действия розовым цветом. Проблема только в том, что он зальет буквально все, включая все созданные ранее эффекты, а также сам текст. Это логично, но чаще всего требуется нечто другое.
Чтобы поправить это, нужно переместить цветной слой назад, «за» текст.
В SVG есть примитив фильтра feMerge, который как раз и отвечает за взаимное расположение нескольких слоев. У него нет атрибута in
, слои указываются как дочерние элементы feMergeNode
. А вот у каждого слоя уже есть атрибут in
, как у обычного примитива.
Слои отображаются именно в порядке перечисления: самый первый слой в коде окажется самым «нижним» (или «дальним») на экране.
<svg width="600px" height="400px" viewBox="0 0 600 400">
<filter id="flooder">
<feFlood flood-color="#EB0066" flood-opacity=".9" result="FLOOD"></feFlood>
<feMerge>
<feMergeNode in="FLOOD" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<text dx="100" dy="200" font-size="150"
font-weight="bold" filter="url(#flooder)">
Effect!
</text>
</svg>
Обратите внимание, результат работы первого примитива имеет псевдоним FLOOD и используется в одном из слоев примитива feMerge в качестве входных данных. А SouceGraphic
у второго слоя – это ссылка на исходное изображение (текст).
See the Pen Filter Region Visualization with feFlood by Sara Soueidan (@SaraSoueidan) on CodePen.
После этого небольшого введения в мир SVG-фильтров, можно перейти к практике и создать простой эффект drop-shadow (падающей тени).
Применение тени к изображению
Для решения этой конкретной задачи было бы проще и разумнее использовать CSS-фильтр drop-shadow()
. SVG-решение получится гораздо многословнее, но тем не менее мы рассмотрим именно его, как простую точку входа в реализацию более сложных эффектов.
Итак, что вообще такое тень?
Обычно это светло-серый слой позади элемента (иногда с небольшим смещением), который имеет такую же форму, как и сам элемент. Другими словами это его серая размытая копия.
Чтобы создать SVG-фильтр, нужно думать поэтапно.
- Создадим обычную черную копию и применим к ней размытие;
- Раскрасим черную копию в серый цвет;
- Немного сместим ее относительно исходного положения элемента;
- Разместим тень позади элемента.
Для создания черной копии используем альфа-канал исходного изображения (это мы уже умеем – SourceAlpha в качестве входных данных фильтра).
Размытие по Гауссу обеспечит примитив feGaussianBlur. Нужное значение размытия указывается в атрибуте stdDeviatio
n (стандартное отклонение). Если указано одно значение, размытие будет равномерным. Можно указать два значения – одно для горизонтального размытия, второе для вертикального. Мы используем обычный равномерный эффект.
<svg width="600" height="400" viewBox="0 0 850 650">
<filter id="drop-shadow">
<-- создаем размытую черную копию -->
<feGaussianBlur in="SourceAlpha" stdDeviation="10" result="DROP"></feGaussianBlur>
</filter>
<image xlink:target="_blank" rel="noopener noreferrer" href="image.png" x="0" y="0" width="100%" height="100%" filter="url(#drop-shadow)"></image>
</svg>
Теперь у нас есть размытый альфа-канал исходного изображения. Это просто черный квадрат Малевича прямоугольник, так как изображение не имеет прозрачных фрагментов. Вот он:
Чтобы перекрасить тень, используем уже знакомый примитив feFlood для заливки нужным цветом и композицию (комбинацию слоев), чтобы залить только нужную область.
Композиция – это способ сочетания графического элемента и его фона (всего, что лежит под/за ним). У нас фон – это черный размытый прямоугольник, а элемент – сплошная серая заливка примитива feFlood. Именно в таком порядке, ведь заливка должна оказаться сверху. Если вы не очень хорошо понимаете, о чем идет речь, загляните сюда – здесь есть большая хорошая вводная статья от автора этой оригинальной статьи.
Примитив feComposite имеет атрибут operator, в нем указывается, по какому алгоритму будет производиться наложение слоев. Мы воспользуемся алгоритмом in
– при этому верхний слой (серая заливка) будет отображаться только поверх черного размытого прямоугольника, не выходя за его пределы.
Для работы feComposite требуется два входящих графических элемента. В атрибут in
(не путать с режимом наложения in
) передаем верхний слой (заливку), в in2
– размытый теневой фон.
<svg width="600" height="400" viewBox="0 0 850 650">
<filter id="drop-shadow">
<feGaussianBlur in="SourceAlpha" stdDeviation="10" result="DROP"></feGaussianBlur>
<feFlood flood-color="#bbb" result="COLOR"></feFlood>
<feComposite in="COLOR" in2="DROP" operator="in" result="SHADOW"></feComposite>
</filter>
<image xlink:href="image.png" x="0" y="0"
width="100%" height="100%" filter="url(#drop-shadow)"></image>
</svg>
Здесь мы используем результаты работы примитивов feGaussianBlur и feFlood в качестве входных данных для feComposite.
Теперь нужно немного сместить тень немного по горизонтали и/или вертикали от исходного положения. Предположим, что источник света находится в верхнем левом углу экрана.
Для смещения слоя используем примитив фильтра feOffset. У него есть уже известные нам атрибуты in
и result
. Кроме них есть еще dx
и dy
, которые определяют величину смещения.
Далее необходимо объединить созданную тень и исходное изображение. С этим фокусом мы тоже уже знакомы – нужно использовать feMerge. Первый слой примет тень, второй – само изображение (SourceGraphic).
<svg width="600" height="400" viewBox="0 0 850 650">
<filter id="drop-shadow">
<!-- Получение и размытие альфа-канала - "DROP" -->
<feGaussianBlur in="SourceAlpha" stdDeviation="10" result="DROP"></feGaussianBlur>
<!-- заливка серым - "COLOR" -->
<feFlood flood-color="#bbb" result="COLOR"></feFlood>
<!-- Комбинация DROP и COLOR - "SHADOW" -->
<feComposite in="COLOR" in2="DROP" operator="in" result="SHADOW"></feComposite>
<!-- Сдвиг SHADOW на 20 пикселей вниз и вправо - "DROPSHADOW" -->
<feOffset in="SHADOW" dx="20" dy="20" result="DROPSHADOW"></feOffset>
<!-- объединение тени и исходного изображения -->
<feMerge>
<feMergeNode in="DROPSHADOW"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<!-- Применение фильтра к изображению -->
<image xlink:href="image.png" x="0" y="0" width="100%" height="100%"
filter="url(#drop-shadow)"></image>
</svg
Вот так это выглядит вживую.
See the Pen Drop Shadow: Tinted shadow with feComposite by Sara Soueidan (@SaraSoueidan) on CodePen.
Вот мы и создали наш первый полноценный SVG-фильтр – ничего сложного. И работает он во всех основных браузерах.
Есть другой способ…
Существует другой, более распространенный способ создания тени. Вместо того, чтобы перекрашивать черную тень в серый для осветления, можно сделать ее полупрозрачной.
Разумеется, если тень должна быть цветной, без feFlood не обойтись, и в принципе это очень полезный метод, поэтому мы к нему и обратились с самого начала. Теперь посмотрим другой вариант.
Для изменения непрозрачности слоя можно использовать либо примитив feColorMatrix, либо примитив feComponentTransfer. Второй будет подробно разобран в следующих статьях серии, поэтому используем feColorMatrix.
Это сложный и мощный примитив, который заслуживает отдельной подробной статьи – и такая статья есть – вот она, от Una Kravets.
feColorMatrix применяет матричное преобразование к каналам цвета каждого пикселя: R(Красный), G(зеленый), B(синий) и A(Альфа). Базовая матрица выглядит так:
<filter id="myFilter">
<feColorMatrix
type="matrix"
values="R 0 0 0 0
0 G 0 0 0
0 0 B 0 0
0 0 0 A 0 "/>
</feColorMatrix>
</filter>
Каналы R, G, B в матрице мы трогать не будем, изменим только альфа-канал:
<filter id="filter">
<!-- Получение и размытие альфа-канала - "DROP" -->
<feGaussianBlur in="SourceAlpha" stdDeviation="10" result="DROP"></feGaussianBlur>
<!-- Сдвиг SHADOW на 20 вниз и вправо - "DROPSHADOW" -->
<feOffset in="SHADOW" dx="20" dy="20" result="DROPSHADOW"></feOffset>
<!-- уменьшение прозрачности тени до 30% -->
<feColorMatrix type="matrix" in="DROPSHADOW" result="FINALSHADOW"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 0.3 0">
</feColorMatrix>
<!-- объединение тени и исходного изображения -->
<feMerge>
<feMergeNode in="FINALHADOW"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
Выглядит это вот так:
See the Pen Drop Shadow: Translucent shadow with feColorMatrix by Sara Soueidan (@SaraSoueidan) on CodePen.
Заключение
В этой серии статей не будет подробного технического описания действия фильтров. Для начала работы достаточно просто в целом понимать, как они работают и как их применить. Чтобы получить более подробную информацию, загляните в спецификацию.
0 комментариев