Эта статья написана по следам создания плагина для чтения SVG файлов для анимационного векторного редактора NanoFL. В ней вы найдёте некоторые особенности того, как устроены файлы SVG изнутри и какие проблемы возникают при их разборе и последующем выводе на HTML5/Canvas средствами JavaScript.


Формат SVG стал по факту стандартом на обмен векторной графикой между различными программами. Поэтому, было решено сделать его поддержку в NanoFL. В процессе подобной разработки важно контролировать правильность чтения и интерпретации файлов, для чего с сайта w3.org была скачана коллекция SVG-изображений различной тематики.

Общие сведения о том, что находится внутри файлов SVG


Как все, наверное, знают SVG-файлы содержат простые текстовые данные в формате XML. Поэтому, их, теоретически, можно открывать и редактировать в обычном текстовом редакторе. Конечно, на практике это имеет смысл лишь для быстрых правок в небольших файлах.

Вот обзор того, что может содержать SVG-файл:

  1. Заголовок (<svg>).
  2. Определения (<defs>) — здесь хранятся описания градиентов и объектов, которые не нужно выводить непосредственно (сразу).
  3. Группа (<g>) — это контейнер для рисуемых элементов, на который можно сослаться при необходимости.
  4. Векторная фигура (<path>) — набор правил для рисования ("перейти в заданную точку", "нарисовать отрезок из текущей позиции в заданную точку"). Поддерживаются как прямые отрезки, так и кривые Безье 2-го и 3-го порядка, а также дуги окружности.
  5. Тест (<text>) — однострочный текст (в SVG нет полноценной поддержки многострочных текстов).
  6. Градиент (<linearGradient>, <radialGradient>).
  7. Область отсечения (<clipPath>) — задаёт фигуру, которая может использоваться для отсечения при выводе.
  8. Узор (<pattern>) — задаёт фигуру, которой можно заливать замкнутые области.
  9. Маска (<mask>) — задаёт объект, alpha-значения пикселей которого можно использовать при выводе произвольного объекта.
  10. Фильтр (<filter>) — один из предопределённых способов пост-обработки заданного графического элемента (например, его размытие).
  11. Растровая картинка (<image>).
  12. Анимация (её поддержка не была реализована в NanoFL и потому не рассматривается в этой статье).

Ниже мы рассмотрим особенности некоторых элементов, упомянутых выше.

Заголовок


Обычно имеет следующий вид:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" viewBox="0 0 100 100"> 

Практические особенности:

  • xmlns:xlink="http://www.w3.org/1999/xlink" — без этого атрибута мы не сможем ссылаться внутри файла на те или иные его элементы (а это нужно, например, если мы хотим несколько раз вывести одну и ту же фигуру или назначить элементу градиент для заливки);
  • width/height — довольно важные атрибуты — задают физические размеры холста, на котором будет выводиться изображение; почти всегда их лучше указывать (и лучше в пикселях), иначе некоторые программы не смогут понять, какого размера картинку мы ожидаем получить при растеризации;
  • viewBox — задаёт координаты и размеры "окна видения"; используется браузерами для позиционирования картинки в области рендеринга (т.е. на холсте), может игнорироваться другими программами.

Группы


Группа — это тег-контейнер, внутри которого могут быть практически любые элементы. Используется как правило или для того, чтобы задать некоторые параметры (например, заливку, область отсечения или фильтр) сразу для всех внутренних элементов, или чтобы впоследствии иметь возможность сослаться на эти элементы. Группа напоминает символ в библиотеке редактора, который мы можем использовать несколько раз, причём видоизменяя его при использовании (для того, чтобы сослаться на группу, используется тег <use>). Ниже приведён пример задания группы и использования ссылок на неё.


<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100">
    <defs>
        <g id="man">
            <circle cx="16" cy="25" r="7"/>
            <path d="m7,44 a9,9 0,0,1 18,0 v20h-18z"/>
        </g>
    </defs>

    <use x="0" y="0" xlink:href="#man" />
    <use x="50" y="-18" transform="rotate(15)" fill="blue" xlink:href="#man" />
</svg>

Векторные фигуры


Для определения векторных фигур в SVG используется несколько тегов: универсальный <path> и частные <polygon>, <circle> и <rect>. Интересны следующие моменты:

  1. Линии фигур могут пересекаться (что неприемлемо как Flash Pro, так и для NanoFL).
  2. SVG поддерживает два различных метода определения того, какую из получившихся замкнутых областей считать внутренней (и, соответственно, закрашивать):
    a) fill-type="nonzero" — из точки внутри области выпускается луч и отдельно считается количество его пересечений с линиями идущими по часовой стрелке и против часовой стрелки; область закрашивается, если полученные числа не совпадают;
    b) fill-type="evenodd" — из точки внутри области выпускается луч и считается количество пересечений с линиями, задающими границу; область закрашивается, если количество пересечений нечётно.

Обе эти особенности были учтены при реализации в NanoFL: все линии разбиваются по точкам пересечений и фигура реконструируется в соответствии с указанным методом выбора варианта закраски. Если же вам вам нужно просто вывести объект на canvas, то проблемы нет вообще: Context2D.fill() поддерживает как фигуры с самопересечениями, так и указание метода закраски.


<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"  width="500" height="300">
    <defs>
        <g id="g" fill="orange" stroke="black">
            <polygon points="47.773,241.534 123.868,8.466 200.427,241.534 7.784,98.208 242.216,98.208" />
        </g>
    </defs>

    <use x="0" y="0" xlink:href="#g" fill-rule="nonzero" />
    <use x="250" y="0" xlink:href="#g" fill-rule="evenodd" />
</svg>

Узоры


Закрашивать замкнутые области в SVG можно произвольным векторным узором. На canvas такое невозможно: Context2D поддерживает создание pattern-ов, однако, в их качестве предлагается использовать растровые изображения, а не векторные. Обходным манёвром здесь может быть растеризация вектора на canvas в памяти с последующей заливкой им заданной области. Однако, в общем случае, это приведёт к серьёзной потере производительности (т.к. невозможно растрировать узор заранее и использовать везде из-за незнания масштаба заливаемого объекта). Поэтому, помня о том, что узоры довольно редко используются в реальных художественных векторных изображениях, было решено отказаться от их поддержки в NanoFL.

Заключение


SVG поддерживает богатый набор возможностей для вывода векторных изображений, однако, не все они имеют прямое отображение в методы, связанные с canvas. Поэтому, где-то есть смысл в написании умного кода, который сделает "как на SVG", а где-то, возможно, стоит отказаться от поддержки функциональности в угоду производительности.

Автор будет рад любым конструктивным комментариям.

Комментарии (7)