Мыльные пузыри на 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="0 0 200 200" width="150" height="150": он нужен затем, чтобы при изменении размеров изображения внутренняя система координат ресайзилась вместе с ним, и элементы не расползались кто куда, а оставались на своих местах. Особенно это важно для координат, которые не могут быть заданы в процентах.

В примерах тег svg будет опущен, но не забудьте его добавить, если будете использовать примеры кода из статьи.

Разноцветный слой

Начнём с самого главного (и самого верхнего) слоя, он имитирует поверхность пузыря с радужными разводами:

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

Как сделать:

  1. Создаём фигуру с разноцветным градиентом

    Пишем градиент:

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

    Получается разноцветная основа, которой не хватает лёгкости и прозрачности.

  2. Добавляем прозрачность

    Для этого воспользуемся радиальным градиентом с черными и белыми оттенками разной степени прозрачности (значения подбирались опытным путём):

    <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, я подумала, что пузырю можно добавить внутренние отражения вверху и внизу.

Отражения

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

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

Как сделать:

  1. Создаём фигуры с однотонной заливкой
    <circle r="50%" cx="50%" cy="50%"
        fill="aqua"> <!-- голубая заливка -->
    </circle>
    
    <circle r="50%" cx="50%" cy="50%"
        fill="yellow"> <!-- желтая заливка -->
    </circle>
  2. Добавляем прозрачность с помощью масок

    В этом градиенте так же используются белый и чёрный разной степени прозрачности, а центральный цвет сдвинут вверх с помощью атрибута 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>

Объединим слои. Отражения внутренние, поэтому слои с ними располагаются под разноцветным слоем:

Пузырь стал более многоцветным, но сейчас его поверхность выглядит матовой. Чтобы сделать её блестящей, нужно добавить блики.

Блики

Они сделаны двумя эллипсами с градиентной заливкой:

Контурная обводка показывает границы пузыря.

Как сделать:

  1. Создаём градиент

    Он состоит из оттенков белого разной степени прозрачности, центральный цвет сдвинут к верхнему краю:

    <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>
  2. Рисуем эллипсы

    Один располагаем вверху слева, другой — внизу справа, добавляем градиентную заливку:

    <!-- Верхний блик -->
    <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>

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

  3. Добавляем трансформации

    Чтобы блики визуально находились на поверхности пузыря, их надо немного повернуть:

    <!-- Верхний блик -->
    <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-градиентов, поделитесь ссылками в комментариях.

Если вы нашли ошибку или неточность, вы можете отредактировать статью с помощью prose.io, а также можно написать мне в комментариях или в Twitter.
Система комментирования от Disqus