В основе работы с библиотекой Snap.svg лежит понимание того, что SVG-документ представляет собой древовидную структуру. Каждый элемент (прямоугольник, круг, группа и т.д.) может быть родителем или потомком других элементов. Это дерево аналогично DOM-структуре HTML, но с особенностями, характерными для векторной графики.
Типичная иерархия выглядит следующим образом:
<svg><g><rect>,
<circle>, <path>,
<text> и др.)Группы (g) играют ключевую роль, так как позволяют
объединять элементы и управлять ими как единым целым.
Snap.svg предоставляет удобные методы для выбора элементов:
var s = Snap("#svg");
var rect = s.select("rect");
var allCircles = s.selectAll("circle");
select() возвращает первый найденный элементselectAll() — коллекцию элементовЭти методы используют CSS-селекторы, что делает их гибкими и мощными.
Каждый элемент Snap имеет ссылку на своего родителя:
var parent = element.parent();
Это позволяет подниматься вверх по дереву, например, чтобы определить, в какой группе находится элемент.
Для получения дочерних элементов используется метод:
var children = element.children();
Результат — массив элементов, входящих в текущий узел.
Методы select() и selectAll() работают не
только от корня, но и относительно любого элемента:
var group = s.select("g");
var innerRects = group.selectAll("rect");
Таким образом реализуется локальный обход поддерева.
Коллекции, возвращаемые selectAll(), поддерживают метод
forEach():
s.selectAll("circle").forEach(function(el) {
el.attr({ fill: "red" });
});
Это основной способ последовательного обхода элементов.
Для полного обхода всех узлов используется рекурсия:
function traverse(element) {
console.log(element.type);
element.children().forEach(function(child) {
traverse(child);
});
}
traverse(s);
Особенности:
element.type содержит тип SVG-элемента
(rect, circle, g и т.д.)В процессе обхода часто требуется работать только с определёнными типами элементов:
function traverse(element) {
if (element.type === "rect") {
element.attr({ fill: "blue" });
}
element.children().forEach(traverse);
}
Фильтрация может основываться на:
element.type)element.attr("class"))element.data())Snap.svg позволяет привязывать данные к элементам:
element.data("id", 123);
При обходе:
if (element.data("id") === 123) {
// обработка
}
Это удобно для построения логики, связанной с состоянием элементов.
g)Группы — ключевой инструмент при обходе:
var group = s.group(rect1, rect2);
При обходе:
Пример:
function colorizeGroup(group) {
group.children().forEach(function(el) {
el.attr({ stroke: "black" });
});
}
Элементы могут иметь трансформации (transform), которые
наследуются:
element.transform("translate(50,50)");
При обходе важно учитывать:
Для анализа используется:
var matrix = element.transform().localMatrix;
Snap.svg позволяет изменять дерево в процессе обхода:
element.remove();
element.appendTo(newParent);
parent.add(newElement);
Важно: при изменении структуры во время обхода следует избегать изменения текущего списка, чтобы не нарушить итерацию.
Рекомендуемый подход:
var children = element.children().slice();
children.forEach(function(child) {
if (child.type === "circle") {
child.remove();
} else {
traverse(child);
}
});
Копия массива (slice()) предотвращает ошибки.
Иногда требуется найти ближайшего родителя с определёнными характеристиками:
function findParent(el, condition) {
var current = el.parent();
while (current) {
if (condition(current)) {
return current;
}
current = current.parent();
}
return null;
}
Пример использования:
var group = findParent(element, function(el) {
return el.type === "g";
});
s.selectAll("*").forEach(function(el) {
el.attr({ opacity: 0.5 });
});
function findWithoutFill(root) {
var result = [];
function traverse(el) {
if (!el.attr("fill")) {
result.push(el);
}
el.children().forEach(traverse);
}
traverse(root);
return result;
}
function countElements(root) {
var count = 0;
function traverse(el) {
count++;
el.children().forEach(traverse);
}
traverse(root);
return count;
}
При работе с большими SVG-документами важно учитывать:
Пример:
var rects = s.selectAll("rect");
Вместо:
traverse(s);
На практике часто используется сочетание методов:
selectAll()Пример:
s.selectAll("g").forEach(function(group) {
group.selectAll("rect").forEach(function(rect) {
rect.attr({ fill: "green" });
});
});
Текст (text) также участвует в дереве:
var text = s.text(10, 10, "Hello");
При обходе:
<tspan>SVG может содержать вложенные <svg> элементы:
var nested = s.select("svg svg");
Альтернатива рекурсии — стек:
function traverseIterative(root) {
var stack = [root];
while (stack.length > 0) {
var el = stack.pop();
console.log(el.type);
stack.push.apply(stack, el.children());
}
}
Преимущества:
function traverseBFS(root) {
var queue = [root];
while (queue.length > 0) {
var el = queue.shift();
console.log(el.type);
queue.push.apply(queue, el.children());
}
}
Snap.svg оборачивает реальные DOM-элементы SVG:
element.node
Это позволяет:
Пример:
console.log(element.node.parentNode);
Для сложных проектов применяются:
Пример паттерна Visitor:
function visit(element, handlers) {
if (handlers[element.type]) {
handlers[element.type](element);
}
element.children().forEach(function(child) {
visit(child, handlers);
});
}
Использование:
visit(s, {
rect: function(el) {
el.attr({ fill: "yellow" });
},
circle: function(el) {
el.attr({ fill: "blue" });
}
});