Мыльные пузыри на SVG
Честно говоря, пузыри у меня получились почти случайно, когда мне потребовалось как следует изучить градиенты и я экспериментировала с их возможностями. И я сама до сих пор не очень понимаю, как так получилось, используя только SVG — векторный формат, — сделать такой невесомый мыльный пузырь.
See the Pen SVG Bubbles by yoksel (@yoksel) on CodePen.
На самом деле, если разобраться, тут нет ничего сложного, а вся магия заключается в использовании масок в сочетании с градиентами.
Подробно про устройство SVG-масок можно почитать здесь, а про градиенты — вот здесь.
Разберём пузырь на слои (от нижнего слоя к верхнему):
Пузырь состоит из пяти слоёв, каждый из которых является фигурой c градиентом, некоторые из них — ещё и с масками (внутри которых тоже градиенты).
Важно: всё содержимое пузыря находится внутри тега svg
:
<svg viewBox="0 0 200 200" width="150" height="150">
<!-- слои будут тут -->
</svg>
Также обратите внимание на обязательный атрибут viewBox
: он нужен затем, чтобы при изменении размеров изображения внутренняя система координат ресайзилась вместе с ним, и элементы не расползались кто куда, а оставались на своих местах. Особенно это важно для координат, которые не могут быть заданы в процентах.
В примерах тег svg
будет опущен, но не забудьте его добавить, если будете использовать примеры кода из статьи.
Разноцветный слой
Начнём с самого главного (и самого верхнего) слоя, он имитирует поверхность пузыря с радужными разводами:
По сути, к фигуре применены два градиента, один из них внутри маски. Эта конструкция позволяет получить фигуру с линейным градиентом и с радиальной прозрачностью одновременно.
Как сделать:
-
Создаём фигуру с разноцветным градиентом
Пишем градиент:
<linearGradient id="gradient--colors" x1="0" y1="100%" x2="100%" y2="0"> <stop offset="0%" stop-color="dodgerblue"/> <stop offset="50%" stop-color="fuchsia"/> <stop offset="100%" stop-color="yellow"/> </linearGradient>
Применяем его к фигуре:
<circle r="50%" cx="50%" cy="50%" fill="url(#gradient--colors)"> <!-- градиентная заливка --> </circle>
Получается разноцветная основа, которой не хватает лёгкости и прозрачности.
-
Добавляем прозрачность
Для этого воспользуемся радиальным градиентом с черными и белыми оттенками разной степени прозрачности (значения подбирались опытным путём):
<radialGradient id="gradient--colors-transparency"> <stop offset="30%" stop-color="black" stop-opacity=".2"/> <stop offset="97%" stop-color="white" stop-opacity=".4"/> <stop offset="100%" stop-color="black"/> </radialGradient>
Чтобы прозрачность выглядела более естественно, нужно немного сместить центр градиента, для этого воспользуемся атрибутами
fx
иfy
:<radialGradient id="gradient--colors-transparency" fx="25%" fy="25%"> <!-- смещение центра градиента --> <stop offset="0%" stop-color="black"/> <stop offset="30%" stop-color="black" stop-opacity=".2"/> <stop offset="97%" stop-color="white" stop-opacity=".4"/> <stop offset="100%" stop-color="black"/> </radialGradient>
Вот что получилось:
Теперь делаем маску. В
mask
кладётся прямоугольник, в качестве заливки используется свежесозданный градиент:<mask id="mask--colors-transparency"> <rect fill="url(#gradient--colors-transparency)" width="100%" height="100%"></rect> </mask>
Затем добавляем маску к фигуре с разноцветной заливкой:
<circle r="50%" cx="50%" cy="50%" fill="url(#gradient--colors)" mask="url(#mask--colors-transparency)"> <!-- маска --> </circle>
Результат:
Весь код разноцветного слоя:
<!-- Разноцветный градиент -->
<linearGradient id="gradient--colors"
x1="0" y1="100%"
x2="100%" y2="0">
<stop offset="0%"
stop-color="dodgerblue"/>
<stop offset="50%"
stop-color="fuchsia"/>
<stop offset="100%"
stop-color="yellow"/>
</linearGradient>
<!-- Градиент прозрачности разноцветного слоя -->
<radialGradient id="gradient--colors-transparency"
fx="25%" fy="25%">
<stop offset="0%"
stop-color="black"/>
<stop offset="30%"
stop-color="black"
stop-opacity=".2"/>
<stop offset="97%"
stop-color="white"
stop-opacity=".4"/>
<stop offset="100%"
stop-color="black"/>
</radialGradient>
<!-- Маска прозрачности разноцветного слоя -->
<mask id="mask--colors-transparency">
<rect fill="url(#gradient--colors-transparency)"
width="100%" height="100%"></rect>
</mask>
<!-- Фигура с маской и разноцветным градиентом -->
<circle r="50%" cx="50%" cy="50%"
fill="url(#gradient--colors)"
mask="url(#mask--colors-transparency)">
</circle>
Получилась полупрозрачная радужная поверхность, пока ещё не очень похожая на мыльный пузырь.
Погуглив картинки по запросу soap bubble, я подумала, что пузырю можно добавить внутренние отражения вверху и внизу.
Отражения
В качестве отражений я использовала две фигуры с однотонной заливкой и маской, добавляющей фигурам прозрачность.
Прозрачность сделана маской, хотя, теоретически, можно было бы обойтись одними градиентами. Но мне хотелось, чтобы отражения были одинаковыми, а кроме того, использование в маске только белого и чёрного цветов позволяет получить более управляемые градиенты.
Как сделать:
-
Создаём фигуры с однотонной заливкой
<circle r="50%" cx="50%" cy="50%" fill="aqua"> <!-- голубая заливка --> </circle> <circle r="50%" cx="50%" cy="50%" fill="yellow"> <!-- желтая заливка --> </circle>
-
Добавляем прозрачность с помощью масок
В этом градиенте так же используются белый и чёрный разной степени прозрачности, а центральный цвет сдвинут вверх с помощью атрибута
fy
:<radialGradient id="gradient--bw-light" fy="10%"> <stop offset="60%" stop-color="black" stop-opacity="0"/> <stop offset="90%" stop-color="white" stop-opacity=".25"/> <stop offset="100%" stop-color="black"/> </radialGradient>
Используя этот градиент, создадим две маски: для верхнего отражения и для нижнего. В маске для верхнего отражения используется тот же градиент, но прямоугольник с градиентом перевёрнут кверх ногами.
<!-- Маска для нижнего отражения --> <mask id="mask--light-bottom"> <rect fill="url(#gradient--bw-light)" width="100%" height="100%"></rect> </mask> <!-- Маска для верхнего отражения --> <mask id="mask--light-top"> <rect fill="url(#gradient--bw-light)" width="100%" height="100%" transform="rotate(180, 100, 100)"></rect> </mask>
Добавляем маски к фигурам с заливкой:
<!-- Нижнее отражение --> <circle r="50%" cx="50%" cy="50%" fill="aqua" mask="url(#mask--light-bottom)"> <!-- маска --> </circle> <!-- Верхнее отражение --> <circle r="50%" cx="50%" cy="50%" fill="yellow" mask="url(#mask--light-top)"> <!-- маска --> </circle>
Результат:
Весь код слоя с отражениями:
<!-- Градиент для прозрачности -->
<radialGradient id="gradient--bw-light" fy="10%">
<stop offset="60%"
stop-color="black"
stop-opacity="0"/>
<stop offset="90%"
stop-color="white"
stop-opacity=".25"/>
<stop offset="100%"
stop-color="black"/>
</radialGradient>
<!-- Маска для нижнего отражения -->
<mask id="mask--light-bottom">
<rect fill="url(#gradient--bw-light)"
width="100%" height="100%"></rect>
</mask>
<!-- Маска для верхнего отражения -->
<mask id="mask--light-top">
<rect fill="url(#gradient--bw-light)"
width="100%" height="100%"
transform="rotate(180, 100, 100)"></rect>
</mask>
<!-- Нижнее отражение -->
<circle r="50%" cx="50%" cy="50%"
fill="aqua"
mask="url(#mask--light-bottom)">
</circle>
<!-- Верхнее отражение -->
<circle r="50%" cx="50%" cy="50%"
fill="yellow"
mask="url(#mask--light-top)">
</circle>
Объединим слои. Отражения внутренние, поэтому слои с ними располагаются под разноцветным слоем:
Пузырь стал более многоцветным, но сейчас его поверхность выглядит матовой. Чтобы сделать её блестящей, нужно добавить блики.
Блики
Они сделаны двумя эллипсами с градиентной заливкой:
Контурная обводка показывает границы пузыря.
Как сделать:
-
Создаём градиент
Он состоит из оттенков белого разной степени прозрачности, центральный цвет сдвинут к верхнему краю:
<radialGradient id="gradient--spot" fy="20%"> <stop offset="10%" stop-color="white" stop-opacity=".7"/> <stop offset="70%" stop-color="white" stop-opacity="0"/> </radialGradient>
-
Рисуем эллипсы
Один располагаем вверху слева, другой — внизу справа, добавляем градиентную заливку:
<!-- Верхний блик --> <ellipse rx="65" ry="25" cx="55" cy="55" fill="url(#gradient--spot)"> </ellipse> <!-- Нижний блик --> <ellipse rx="40" ry="20" cx="150" cy="150" fill="url(#gradient--spot)"> </ellipse>
Так как радиальный градиент применили к эллипсам, он сплющился, чтобы уместиться в фигуре целиком.
-
Добавляем трансформации
Чтобы блики визуально находились на поверхности пузыря, их надо немного повернуть:
<!-- Верхний блик --> <ellipse rx="55" ry="25" cx="55" cy="55" fill="url(#gradient--spot)" transform="rotate(-45, 55, 55)"> <!-- трансформация --> </ellipse> <!-- Нижний блик --> <ellipse rx="40" ry="20" cx="150" cy="150" fill="url(#gradient--spot)" transform="rotate(-225, 150, 150)"> <!-- трансформация --> </ellipse>
Результат:
Весь код слоя с бликами:
<!-- Градиент блика -->
<radialGradient id="gradient--spot" fy="20%">
<stop offset="10%"
stop-color="white"
stop-opacity=".7"/>
<stop offset="70%"
stop-color="white"
stop-opacity="0"/>
</radialGradient>
<!-- Верхний блик -->
<ellipse rx="55" ry="25" cx="55" cy="55"
fill="url(#gradient--spot)"
transform="rotate(-45, 55, 55)">
</ellipse>
<!-- Нижний блик -->
<ellipse rx="40" ry="20" cx="150" cy="150"
fill="url(#gradient--spot)"
transform="rotate(-225, 150, 150)">
</ellipse>
Финальная сборка
Теперь можно собрать весь код в финальную версию. В примерах я объединяла в группы разные компоненты одного слоя, но если теперь просто свалить весь код в одну кучу, во-первых, получится неверная последовательность слоёв, а во-вторых, таким кодом будет просто неудобно пользоваться.
При этом код содержит два типа элементов, и можно удобно сгруппировать элементы по типу. Первый тип — это простые фигуры (в нашем случае — круги и эллипсы), именно они являются видимыми слоями, из которых в итоге получается пузырь. Второй тип — это элементы, которые не отображаются на странице, но влияют на внешний вид отображаемых фигур: это маски и градиенты.
Неотображаемые элементы будет удобно положить в defs
— это такой элемент, специально предназначенный для хранения переиспользуемых штук. Ни сам элемент, ни его содержимое не отображаются на странице.
Отображаемые элементы следует расположить после defs
. При этом нужно обратить внимание на порядок слоёв и помнить, что чем ниже слой в коде, тем ближе он к зрителю, то есть самым первым к коде должен быть нижний блик, а самым верхним — слой с разноцветным градиентом.
Примерный код (маски и градиенты опущены, полный код есть ниже):
<svg viewBox="0 0 200 200" width="150" height="150">
<defs>
<!-- здесь должны быть маски и градиенты -->
</defs>
<!-- Нижний блик -->
<ellipse rx="40" ry="20" cx="150" cy="150"
fill="url(#gradient--spot)"
transform="rotate(-225, 150, 150)">
</ellipse>
<!-- Нижнее отражение -->
<circle r="50%" cx="50%" cy="50%"
fill="aqua"
mask="url(#mask--light-bottom)">
</circle>
<!-- Верхнее отражение -->
<circle r="50%" cx="50%" cy="50%"
fill="yellow"
mask="url(#mask--light-top)">
</circle>
<!-- Верхний блик -->
<ellipse rx="55" ry="25" cx="55" cy="55"
fill="url(#gradient--spot)"
transform="rotate(-45, 55, 55)">
</ellipse>
<!-- Фигура с маской и разноцветным градиентом -->
<circle r="50%" cx="50%" cy="50%"
fill="url(#gradient--colors)"
mask="url(#mask--colors-transparency)">
</circle>
</svg>
И весь пузырь целиком:
See the Pen grEWZw by yoksel (@yoksel) on CodePen.
Обратите внимание, что белые блики выглядят цветными. Так как нижний блик как бы отражается от внутренней поверхности пузыря, и относительно зрителя расположен на самой дальней его стенке, в слоях он находится глубже всех, в самом низу (и первым в коде).
Верхний же блик предполагается на внешней стенке пузыря, ближайшей к зрителю, но чтобы и его тоже немного привести в соответствие по цветам, он был размещён не самым верхним слоем, а под слоем с радужными разводами.
Заключение
Используя сочетание масок и градиентов можно сделать векторный мыльный пузырь. Преимущество вектора перед растром состоит в том, что его можно сколько угодно растягивать и сжимать без потери качества, а кроме того, цвета такого пузыря можно анимировать.
Я думаю, что это не самое точное изображение мыльного пузыря, но фотореализм и не был моей целью — мне хотелось узнать насколько сложными могут быть градиенты и какую пользу можно извлечь из сочетания градиентов и масок. Результаты впечатляют, и я уверена, что используя вышеописанные способы, разработчики с богатой фантазией могут сделать много интересного.
Также, если у вас на примете есть интересные демо с использованием SVG-градиентов, поделитесь ссылками в комментариях.