Как оживить SVG, часть 1

Азы для начинающих


Оглавление



Введение


С момента появления на свет этого сайта, мне очень часто стали задавать вопрос - как все это работает и что нужно сделать, чтобы оживить SVG картинку и заставить ее отображать динамически меняющуюся информацию. Эти вопросы и сподвигли меня написать статью, которую Вы сейчас читаете. Поскольку контингент спрашивающих очень широк - от людей, не знающих даже основ WEB-технологий, до тех, кто давно с ними работает, но у них чего-то не получается, то изложить материал, чтоб он устраивал всех вряд ли получится. Кому-то вся приведенная здесь информация покажется всем известной банальностью, кто-то подчерпнет некоторые нюансы, знания которых им не хватало, а кто-то вообще ничего не поймет. Первым я предлагаю прекратить чтение и приношу извинения за потраченное время, вторым я дал то, что смог, ну а третьим я посоветую все же изучить хотя бы основы HTML, SVG и JavaScript, чтобы потом вернуться к изучению представленного здесь материала.

Наверх

Постановка задачи


Итак, дано: мы имеем простейшее SVG изображение, приведенное на рисунке слева. Это изображение вставлено в HTML страницу посредством тэга <embed>:

<embed width="200" height="100" src="svgdyn.svg" /> Содержимое SVG файла изображения: <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" viewBox="0 0 200 100" preserveAspectRatio="xMidYMid meet" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns="http://www.w3.org/2000/svg"> <rect id="fig1" x="10" y="10" width="80" height="80" fill="red" stroke="black" stroke-width="10" /> <circle id="fig2" cx="150" cy="50" r="40" fill="black" stroke="red" stroke-width="10" /> </svg> Мы видим (а кто не видит - надо ознакомиться хотя бы с основами SVG), что SVG файл содержит два объекта: прямоугольник, имеющий идентификатор fig1, и окружность, имеющую идентификатор fig2. Мы хотим поменять цвет обводки и цвет заливки этих фигур. За цвет обводки отвечает атрибут stroke, за цвет заливки - fill. Для поиска узлов DOM и установки их атрибутов мы будем использовать JavaScript библиотеку jQuery. Использование конкретно данной библиотеки не является принципиальным, все то же самое можно реализовать на любой другой, предоставляющей сходные возможности, и вообще на голом JavaScript и DOM API, просто в последнем случае код получится более громоздким и менее понятным.

Наверх

Решение


Для тех, кто уже использовал jQuery для поиска узлов DOM HTML и изменения их атрибутов, решение кажется тривиальным:

$("#fig1").attr("fill", "black").attr("stroke", "red");
$("#fig2").attr("fill", "red").attr("stroke", "black");

Пишем, запускаем и... Не работает? Правильно! И не будет: дело в том, что DOM HTML страницы и DOM SVG изображения - по сути разные объекты и просто так прыгнуть из одного в другой не получится. Чтобы jQuery смогла обходить узлы DOM SVG, ей надо указать корневой узел, от которого надо начинать поиск. А чтобы указать, надо сначала его найти. В разных браузерах работа с SVG реализована не одинаково, да и сам метод вставки SVG в HTML страницу может быть разным, не обязательно это использованый нами тэг <embed>. Для того, чтобы иметь реализацию, не зависящую от перечисленных нюансов, предлагается пойти обратным путем - пусть сам SVG, после загрузки, найдет наш объект окна, в котором отображается HTML страница и сообщит о себе, ведь поиск от детей к родителям всегда проще, так как у любого узла может быть только один родитель. Чтобы не зависеть от того, в какое место иерархии DOM HTML вставлен SVG, поиск будем вести рекурсивно, пока не найдем нужный элемент (ну или вообще ничего не найдем). Для этого объект окна, в котором отображается HTML страница, расширим функцией svgload_html:

window.svgload_html = function(evt) {
  var svg = $(evt.target), //Вот он, корневой узел DOM SVG
     f = false;
  setInterval(function() {
    f = !f;
    svg.children("#fig1").attr("fill", f ? "black" : "red").attr("stroke", f ? "red" : "black");
    svg.children("#fig2").attr("fill", f ? "red" : "black").attr("stroke", f ? "black": "red");
  }, 1000);
};

Данная функция будет выполнять двоякую роль: во первых, ее наличие - это некий маркер, помечающий искомый объект окна, во вторых - это функция, которая будет вызвана со стороны SVG для передачи в качестве параметра корневого узла своего DOM. Данная функция несколько отличается от того, что фигурировало в начале главы и неработало - для наглядности я добавил таймер, по срабатыванию которого цвета обводки и заливки фигур меняются циклически. SVG файл тоже несколько модифицируем: <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" viewBox="0 0 200 100" preserveAspectRatio="xMidYMid meet" onload="svgload_svg(evt);" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns="http://www.w3.org/2000/svg"> <defs> <script type="application/ecmascript"><![CDATA[ function svgload_svg(evt) { var w = window, w0 = null; while (w && !w.svgload_html && w != w0) { w0 = w; w = w.parent; } if (w) w.svgload_html(evt); } ]]></script> </defs> <rect id="fig1" x="10" y="10" width="80" height="80" fill="red" stroke="black" stroke-width="10" /> <circle id="fig2" cx="150" cy="50" r="40" fill="black" stroke="red" stroke-width="10" /> </svg> Здесь в тэг <svg> мы добавили атрибут onload, значение которого указывает на функцию (в теле SVG, а не HTML!) svgload_svg, которая будет вызвана после полной загрузки SVG документа. Так же мы добавили саму эту функцию, поместив ее описание в тэг <script>, который, в свою очередь, поместили в секцию <defs>, как это рекомендовано спецификацией SVG. Функция svgload_svg производит рекурсивный поиск среди родителей окна, объект которого имеет поле svgload_html, найдя который производит вызов функции svgload_html уже в контексте HTML, передав ей в качестве параметра объект-событие evt, содержащий в поле target корневой узел DOM SVG. Так же мы приняли меры от зацикливания в случае, если искомый объект не существует, например, если документ SVG будет загружен в браузер минуя нашу HTML страницу. Дело в том, что в некоторых браузерах корневой объект окна в качестве родителя указывает сам на себя, что может привести к бесконечному рекурсивному циклу, с последующей выдачей сообщения о том, что скрипт не отвечает и предложением прервать его исполнение. Итак: пишем, запускаем и... Работает! Результат Вы можете видеть на картинке слева.

Наверх

Чистый пример


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

Наверх

PS
Вместо послесловия


Поскольку в этой статье я пытался объяснять материал буквально “на пальцах”, прошу корифеев простить мне допущенные некоторые терминологические неточности.

Наверх

PPS
Для эстетов


Ну и напоследок, чтобы придать нашей картинке некоторую эстетичность, попробую организовать “рисующий свет”, как говорят фотографы.


ссылка для просмотра в новом окне

HTML:

<!DOCTYPE html> <html> <head> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta charset="UTF-8"> <meta name="author" content="Рядков Сергей" /> <meta name="copyright" lang="ru" content="Рядков Сергей" /> <meta name="document-state" content="Static" /> <meta name="robots" content="all" /> <meta http-equiv="content-language" content="ru" /> <link rel="shortcut icon" href="/favicon.ico" type="image/vnd.microsoft.icon" /> <link rel="icon" href="/favicon.ico" type="image/vnd.microsoft.icon" /> <style type="text/css"> body { margin:0; padding:0; background-color:blue; overflow:hidden; } div { position:absolute; left:0; top:0; width:100%; height:100%; background:-webkit-gradient(linear, 0 0, 0 100%, from(navy), to(aqua), color-stop(0.5, blue)); background:-moz-linear-gradient(navy, blue, aqua); } embed { position:absolute; left:50%; top:50%; margin:-100px 0 0 -200px; background:silver; -o-border-radius:10px; -moz-border-radius:10px; -webkit-border-radius:10px; border-radius:10px; box-shadow:5px 5px 10px 2px rgba(0,0,0,0.7); -o-box-shadow:5px 5px 10px 2px rgba(0,0,0,0.7); -moz-box-shadow:5px 5px 10px 2px rgba(0,0,0,0.7); -webkit-box-shadow:5px 5px 10px 2px rgba(0,0,0,0.7); } </style> <!--[if lt IE 9]> <script src="/js/html5.js"></script> <script src="/js/excanvas.js"></script> <![endif]--> <script type="text/javascript" src="/js/jquery-2.0.2.min.js"></script> <script type="text/javascript" src="/js/jquery-migrate-1.2.1.min.js"></script> <script type="text/javascript"> window.svgload_html = function(evt) { var svg = $(evt.target), f = false, f1=svg.find("#fig1"), f2=svg.find("#fig2"); setInterval(function() { f = !f; f1.attr("fill", f ? "black" : "red").attr("stroke", f ? "red" : "black"); f2.attr("fill", f ? "red" : "black").attr("stroke", f ? "black": "red"); }, 1000); }; </script> <title>Мнемосхемы SVG - Пример динамики</title> </head> <body> <div> <embed width="400px" height="200px" src="svgdyn3.svg" /> </div> </body> </html> SVG: <?xml version="1.0" encoding="utf-8"?> <svg version="1.1" viewBox="0 0 200 100" preserveAspectRatio="xMidYMid meet" onload="svgload_svg(evt);" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns="http://www.w3.org/2000/svg"> <defs> <script type="application/ecmascript"><![CDATA[ function svgload_svg(evt) { var w = window, w0 = null; while (w && !w.svgload_html && w != w0) { w0 = w; w = w.parent; } if (w) w.svgload_html(evt); } ]]></script> <filter id="light" filterUnits="userSpaceOnUse" x="0" y="0" width="220" height="120"> <feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/> <feOffset in="blur" dx="4" dy="4" result="offsetBlur"/> <feSpecularLighting in="blur" surfaceScale="5" specularConstant=".75" specularExponent="20" lighting-color="#bbbbbb" result="specOut"> <fePointLight x="-5000" y="-10000" z="20000"/> </feSpecularLighting> <feComposite in="specOut" in2="SourceAlpha" operator="in" result="specOut"/> <feComposite in="SourceGraphic" in2="specOut" operator="arithmetic" k1="0" k2="1" k3="1" k4="0" result="litPaint"/> <feMerge> <feMergeNode in="offsetBlur"/> <feMergeNode in="litPaint"/> </feMerge> </filter> </defs> <g filter="url(#light)"> <g id="fig1" fill="red" stroke="black"> <rect x="10" y="10" width="80" height="80" rx="20" ry="20" fill="none" stroke-width="10" /> <rect x="20" y="20" width="60" height="60" rx="10" ry="10" stroke="none" /> </g> <g id="fig2" fill="black" stroke="red"> <circle cx="150" cy="50" r="40" fill="none" stroke-width="10" /> <circle cx="150" cy="50" r="30" stroke="none" /> </g> </g> </svg>

Наверх
Продолжение