Странности обводки в SVG

Экспериментируя с SVG можно обнаружить много странных моментов. Получить странное при манипуляциях с масштабированием ещё как-то ожидаемо, но внезапно сюрпризы преподнесло свойство stroke-width.

stroke-width задает тощину обводки для фигуры. Если в HTML рамка отрисовывается от внешнего края внутрь фигуры и увеличивает её размеры (что можно исправить с помощью box-sizing), то в SVG обводка ведет себя иначе: во-первых, она не растягивает фигуру, во-вторых она отрисовывается и внутрь, и наружу относительно внешнего края фигуры.

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

See the Pen rKpFk by yoksel (@yoksel) on CodePen.

Круг с диаметром 60px, квадрат — 60х60px.

С помощью CSS добавим фигурам рамку:

stroke: yellowgreen;
stroke-width: 30;
stroke-opacity: .5;

Рамка зеленого цвета, толщиной 30 пикселей. Чтобы видеть как она накладывается на фигуру, рамке задана полупрозрачность (stroke-opacity: .5).

Результат:

See the Pen omKIc by yoksel (@yoksel) on CodePen.

Очевидно, что рамка отрисовывается от внешнего края фигуры в обоих направлениях: и внутрь, и наружу.

Обычная рамка ведет себя довольно понятно и предсказуемо, но самое интересное начинается, если задать рамку, превышающую ширину фигуры больше чем в два раза. Чем больше разница — тем интересней результат. Чтобы было хорошо видно, я задала рамку равную трехкратному значению ширины (60 * 3 = 180):

See the Pen AkaKc by yoksel (@yoksel) on CodePen.

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

Чем больше разница между значениями, тем больше белые поля и тем тоньше рамка (радиус 10, толщина обводки — 200):

See the Pen SVG: r=10 + stroke-width=150 by yoksel (@yoksel) on CodePen.

Совсем не то, что хотелось бы получить.

Ещё более интересный результат получается, если добавить свойство stroke-dasharray.

Код рамки:

stroke-width: 180;
stroke: yellowgreen;
stroke-opacity: .5;
stroke-dasharray: 5;

Результат:

See the Pen vDpqg by yoksel (@yoksel) on CodePen.

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

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

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

Зачем это может быть нужно?

Предположим, есть круглая маска и картинка под ней:

<svg>
    <mask id="mask">
        <circle r="100" cx="100" cy="100"
                class="c-mask-circle"/>
    </mask>

  <g mask="url(#mask)">
    <image xlink:href="http://colourlovers.com.s3.amazonaws.com/images/patterns/2693/2693008.png"
           width="100%" height="100%"/>
  </g>
</svg>

See the Pen mpvhb by yoksel (@yoksel) on CodePen.

Я хочу анимировать маску, чтобы она пропорционально растягивалась и сужалась относительно центра картинки. Первым делом в голову, конечно, приходит transform. Код анимации:

@keyframes scale {
  50% {
    transform: scale(10);
  }
}

Подключаем анимацию:

.c-mask-circle {
  fill: white;
  animation: scale 5s infinite;
}

See the Pen dKrft by yoksel (@yoksel) on CodePen.

Так как центр трансформаций в SVG находится в левом верхнем углу области отрисовки, фигура движется совсем не так, как хотелось бы. Задаем центр трансформаций, который нужен нам:

transform-origin: 100px 100px;

И всё вместе:

.c-mask-circle {
  fill: white;
  transform-origin: 100px 100px;
  animation: scale 5s infinite;
}

@keyframes scale {
  50% {
    transform: scale(10);
  }
}

Результат:

See the Pen fKqAI by yoksel (@yoksel) on CodePen.

В Chrome анимация будет работать как задумано: маска увеличивается от центра и затем уменьшается в обратном направлении. В Firefox она не будет работать вообще: Firefox не анимирует трансформации внутри масок.

Способ с трансформацией не работает. Какие ещё варианты есть? Вот как раз в этом случае можно попробовать анимировать толщину обводки.

Почему это сработает?

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

Для примера возьмем кружок c обводкой и используем его в mask и clipPath:

See the Pen ouLIh by yoksel (@yoksel) on CodePen.

Для фигур внутри clipPath можно было бы попробовать анимировать transform, но они тоже не анимируются в Firefox. Таким образом, в нашем распоряжении остаются только маски и манипуляции с обводками. Конец лирического отступления.

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

<mask id="mask">
    <circle r="10" cx="100" cy="100"
            class="c-mask-circle"/>
</mask>

Задаю ей белую заливку и добавляю обводку:

.c-mask-circle {
  fill: white;
  stroke: white;
  stroke-width: 1;
}

Добавляю анимацию толщины обводки:

@keyframes stroke-width {
  50% {
    stroke-width: 170;
  }
}

Весь итоговый код:

.c-mask-circle {
  fill: white;
  stroke: white;
  stroke-width: 1;
  animation: stroke-width 5s infinite;
}

@keyframes stroke-width {
  50% {
    stroke-width: 170;
  }
}

Результат:

See the Pen oebyE by yoksel (@yoksel) on CodePen.

В отличие от способа с transform, обводка замечательно анимируется в Firefox, и это можно было бы использовать, если бы не ограничение на толщину обводки, описанное выше.

Таким образом, с одной стороны, задавать толстую рамку для маленького элемента кажется странной идеей, с другой — это можно было бы использовать в практических целях, так что жаль, что stroke-width отрисовывается не так как хотелось бы.

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

Ещё интереснее ведет себя Safari: в нем фигура маски цельная, а результат применения маски к картинке — с зазорами.

Таким образом, при желании получить анимированую маску, можно сочетать анимацию transform для Хрома (и других webkittens) с анимацией stroke-width для Firefox, например: так:

See the Pen Animated mask for Firefox and webkittens by yoksel (@yoksel) on CodePen.

Способ странный, но это работает.

Правда, обе анимации не будут работать в IE, потому что он вообще не поддерживает анимации в SVG.

Анимируя stroke-width в маске можно делать разные другие интересные штуки, но это уже тема для отдельного поста.

Версии браузеров на момент написания поста: Chrome 36, Firefox 31, Safari 7, Opera 23, IE 11.

Картинка с пионами отсюда.

Ссылки по теме:
Clipping, Masking and Compositing
Stroke Properties
CSS и SVG маски
SVG-фигуры и трансформации
Если вы нашли ошибку или неточность, вы можете отредактировать статью с помощью prose.io, а также можно написать мне в комментариях или в Twitter.
Система комментирования от Disqus