Как стать автором
Обновить

Рисуем кнопку в SVG

Время на прочтение7 мин
Количество просмотров8.6K
В настоящее время я работаю над одним веб-приложением, и вот захотелось мне обновить нынешний, довольно-таки топорный интерфейс на что-то более современное, более красивое. Начать решил с кнопок как с наиболее технически нагруженной части: в них требуется не только заменить внешний вид, но и добавить индикацию нажатия и обработку событий. Сразу же возникла проблема: как обеспечить масштабирование? Обычной растровой картинкой не обойтись, так как у пользователей могут использоваться разные шрифты (как вид, так и размер), и картинка-подложка не будет под них адаптирована. Логично было бы попробовать использовать для этих целей SVG, чем я и занялся.

К сожалению, в конечном итоге я пришёл к выводу, что овчинка не стоит выделки: слишком много проблем вылезло при попытке реализации этой идеи. Тем не менее, я не считаю это время потерянным: я приобрёл новые знания и навыки и теперь хотел бы поделиться ими с сообществом, чтобы облегчить жизнь тем, кто решит повторить мой путь. Описать свои мучения я планирую в двух статьях: в первой — работа над собственно SVG-картинкой, во второй — техника внедрения полученной картинки в качестве кнопок, возникающие при этом проблемы и их решение или обходные пути. Кому интересна первая часть, прошу под кат.

Для начала хочу предупредить, что область SVG для меня совершенно новая, и полностью изучить все мыслимые функции и всякие хитрые трюки я не имел возможности. Соответственно, если в тексте встречается фраза вида «это сделать невозможно», считайте её просто сокращённой формой от «я не нашёл способа сделать это ни в стандарте, ни в учебниках, ни в гугле». :-) Естественно, всяческим поправкам и уточнениям буду очень благодарен.

Ну а теперь приступим к ТЗ: хочется иметь красивую масштабируемую картинку, которую можно было бы использовать в качестве кнопки на веб-страницах. В качестве основы для экспериментального дизайна я выбрал приглянувшуюся мне кнопочку с сайта One day files:

Пример кнопки

Немного поразбиравшись с картинкой, я составил её векторное описание: по границе — серый прямоугольник с закруглёнными краями, внутри — вертикальный градиент, состоящий из двух линейных областей (50%/50%). Получилась элементарнейшая задача для первого класса занятия по изучению SVG; решение может выглядеть следующим образом:
<defs>
  <linearGradient id="btnGrad" x1="0%" y1="0%" x2="0%" y2="100%">
    <stop offset="0%"   stop-color="#777777" />
    <stop offset="50%"  stop-color="#303030" />
    <stop offset="100%" stop-color="#1c1c1c" />
  </linearGradient>
</defs>
<rect style="fill: url(#btnGrad); stroke: #7f7f7f; stroke-width: 1px;"
      x="0px" y="0px" width="100%" height="100%" rx="5px" />

(Здесь и далее я для экономии места буду опускать XML-заголовок и определение тега <svg>.)

Однако здесь не всё так просто. Вот как выглядит то, что получилось:

Результат-1

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

Результат-1а

Результат-1а (увеличенный)

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

Вся беда в том, что векторные координаты находятся не в центрах пикселей, а «в промежутках» между ними. Естественно, при отрисовке такая линия «размывается» между двумя соседними положениями вдоль всей своей длины, причём одна из «половинок» линии оказывается за пределами рисунка и не отображается. Если бы у нас был обычный прямоугольник, можно было бы объявить это фичей, подправив цвет на более яркий (для компенсации размытия) или вообще установив толщину границы в два пикселя. Однако в нашем случае проблему это не решит, закруглённые уголки всё портят: они-то находятся целиком в области видимости, поэтому и переход останется угловатым, и толщина линии в закруглениях останется «полноценной», в отличие от боковых границ. Как быть? Можно попытаться сдвинуть координаты на половину пикселя, это решит проблему «междупиксельности». Но ширину и высоту мы в этом случае корректно задать уже не сможем: невозможно указать значение «сто процентов минус один пиксель». Задавать какие-нибудь 95% бессмысленно: для маленьких кнопок разница в 5% будет слишком мала, для больших — слишком велика. Если же указывать все размеры только в пикселях, то мы не сможем обеспечить необходимый режим масштабирования: толщина границы тут же окажется привязанной к размерам изображения и будет меняться пропорционально. В результате, если мы растянем картинку по горизонтали, оставив высоту неизменной (типичный вариант использования кнопок на веб-странице), то боковые границы станут толще, чем верхняя и нижняя, что выглядит очень некрасиво.

Основная проблема здесь в том, что мы пытаемся совместить процентные и пиксельные координаты, масштабируемую часть с фиксированной частью изображения, что в SVG как бы не принято. Более того, как выяснилось, процентные координаты можно указывать только для фигур. Тег <path> их не поддерживает в принципе (несмотря на заверения учебников, что все фигуры — это всего лишь частные случаи <path>). Что ж, попытаемся обойтись обычными фигурами, и в этом нам поможет атрибут transform. Трюк заключается в том, что мы установим начальную координату в 100%, ширину и высоту укажем фиксированную — в пикселях, а полученную фигуру (находящуюся за пределами рисунка) вдвинем обратно в изображение, сместив его на нужное число пикселей. Например, чтобы нарисовать прямоугольник размерами 5×10 в нижнем левом углу, можно использовать следующий код:
<rect x="0px" y="100%" width="5px" height="10px" transform="translate(0, -10)" />

К сожалению, даже этот трюк не поможет нарисовать сразу всю границу одним тегом <rect>, но мы можем, комбинируя простые фигуры, нарисовать маску для той области, где должна проходить граница, а потом выполнить заливку. Конкретный способ рисования маски, разумеется, может варьироваться, но я сделал так: двумя прямоугольными рамками, сдвинутыми на полпикселя, обрисовал общий прямоугольник границы. Далее вырезал угловые участки, посадил на их место кружочки и, наконец, отрезал лишние части этих кружочков, оставив лишь по одной четверти для закругления углов. Вот итоговый код:
<mask id="boundRect">
  <rect style="fill: none; stroke: #ffffff; stroke-width: 1px;"
        x="0.5px"  y="0.5px"  width="100%" height="100%" />
  <rect style="fill: none; stroke: #ffffff; stroke-width: 1px;"
        x="-0.5px" y="-0.5px" width="100%" height="100%" />
  <g style="fill: #000000;">
    <rect x="0px"  y="0px"  width="5px" height="5px" transform="translate(0, 0)" />
    <rect x="0px"  y="100%" width="5px" height="5px" transform="translate(0, -5)" />
    <rect x="100%" y="0px"  width="5px" height="5px" transform="translate(-5, 0)" />
    <rect x="100%" y="100%" width="5px" height="5px" transform="translate(-5, -5)" />
  </g>
  <g style="fill: #ffffff;">
    <circle cx="5px"  cy="5px"  r="5px" transform="translate(0, 0)" />
    <circle cx="5px"  cy="100%" r="5px" transform="translate(0, -5)" />
    <circle cx="100%" cy="5px"  r="5px" transform="translate(-5, 0)" />
    <circle cx="100%" cy="100%" r="5px" transform="translate(-5, -5)" />
  </g>
  <g style="fill: #000000;">
    <circle cx="5px"  cy="5px"  r="4px" transform="translate(0, 0)" />
    <circle cx="5px"  cy="100%" r="4px" transform="translate(0, -5)" />
    <circle cx="100%" cy="5px"  r="4px" transform="translate(-5, 0)" />
    <circle cx="100%" cy="100%" r="4px" transform="translate(-5, -5)" />
    <rect x="1px"  y="5px"  width="9px" height="5px" transform="translate(0, 0)" />
    <rect x="5px"  y="1px"  width="5px" height="9px" transform="translate(0, 0)" />
    <rect x="1px"  y="100%" width="9px" height="5px" transform="translate(0, -10)" />
    <rect x="5px"  y="100%" width="5px" height="9px" transform="translate(0, -10)" />
    <rect x="100%" y="5px"  width="9px" height="5px" transform="translate(-10, 0)" />
    <rect x="100%" y="1px"  width="5px" height="9px" transform="translate(-10, 0)" />
    <rect x="100%" y="100%" width="9px" height="5px" transform="translate(-10, -10)" />
    <rect x="100%" y="100%" width="5px" height="9px" transform="translate(-10, -10)" />
  </g>
</mask>

Конечно, в качестве вырезания нужных четвертей из кругов было бы логичнее воспользоваться маской или обрезкой через <clipPath>, но оказалось, что при таком вложенном использовании Оперу версий ниже 11 начинает здорово колбасить: начальная отрисовка выполняется корректно, но если поверх неё протащить другое окно, обновление изображения выполняется некорректно. Проще оказалось отказаться от вложенных масок и замостить обычными прямоугольничками, чем бодаться с этим глюком.

Собственно, кнопка уже практически готова. Осталось лишь навесить текст, но и здесь полезли неприятности. Первая, она же главная: каким должен быть размер шрифта? Простого решения этого вопроса не нашлось, а поскольку он имеет самое прямое отношение к интеграции SVG-изображения с HTML-страницей, я отложу эту рассмотрение этой проблемы до следующей статьи, а пока для иллюстративного примера оставлю размер шрифта умолчальным. Также хотелось получить отцентрированный текст, но даже такая простая задача оказалась не под силу некоторым браузерам (не будем указывать пальцем, хотя это Firefox), которые не умеют подстраивать базу шрифта, поэтому придётся пока обойтись центрированием лишь по горизонтали.

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

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

Благодарю за внимание, готов ловить помидоры. :-)

UPD: Вторая часть статьи
Теги:
Хабы:
Всего голосов 41: ↑38 и ↓3+35
Комментарии49

Публикации

Истории

Ближайшие события

19 сентября
CDI Conf 2024
Москва
24 сентября
Конференция Fin.Bot 2024
МоскваОнлайн
30 сентября – 1 октября
Конференция фронтенд-разработчиков FrontendConf 2024
МоскваОнлайн