Корректное масштабирование SVG-элементов

HTML и CSSXSLTJavaScriptИзображенияСофтEtc
Влад Яковлев Анатоль Латотин

11 декабря 2009


Задача.

Правильно масштабировать SVG-элементы в браузерах на движке WebKit при масштабировании страницы.

Ошибки бывают и в браузерах на движке WebKit: при масштабировании страницы Safari и Chrome неправильно изменяет размеры SVG-элементов (Firefox и Opera этой проблемы не имеют). Увидеть это можно на примере (в режиме масштабирования всей страницы, а не только текста).

Код примера:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
$(function() {

    var
        coords = [0, 0, 300, 300],
        svgEl,
        width = 300,
        height = 300,
        svgNs = 'http://www.w3.org/2000/svg';

    // Ему тут делать нечего…
    if ($.browser.msie) return;

    createSVG();

    function createSVG() {
        var rootEl = $('.svg_container1');

        svgEl = document.createElementNS(svgNs, 'svg');
        svgEl.setAttribute('version', '1.1');
        svgEl.setAttribute('class', 'shape');
        svgEl.setAttribute('viewBox', '0 0 300 300');
        svgEl.setAttribute('width', width);
        svgEl.setAttribute('height', height);
        rootEl.append(svgEl);

        var lineEl = document.createElementNS(svgNs, 'line');
        lineEl.setAttribute('id', 'lineEl');
        lineEl.setAttribute('x1', coords[0]);
        lineEl.setAttribute('y1', coords[1]);
        lineEl.setAttribute('x2', coords[2]);
        lineEl.setAttribute('y2', coords[3]);
        lineEl.setAttribute('stroke', '#ff0000');
        svgEl.appendChild(lineEl);
    }
});

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

При изменении значения коэффициента всем элементам с тегом svg нужно менять атрибуты:

— ширину и высоту умножать на коэффициент;
— размеры viewBox делить на коэффициент.

Посмотрим, что получилось:

Код примера:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
$(function() {

    var
        coords = [0, 0, 300, 300],
        svgEl,
        width = 300,
        height = 300,
        svgNs = 'http://www.w3.org/2000/svg';

    // Ему тут делать нечего…
    if ($.browser.msie) return;

    createSVG();

    webkitSvgFix.bind(function(factor) {
        svgEl.setAttribute('width', width * factor);
        svgEl.setAttribute('height', height * factor);
        svgEl.setAttribute('viewBox', [coords[0], coords[1], coords[2] / factor, coords[3] / factor].join(' '));
    });

    function createSVG() {
        var rootEl = $('.svg_container2');

        svgEl = document.createElementNS(svgNs, 'svg');
        svgEl.setAttribute('version', '1.1');
        svgEl.setAttribute('class', 'shape');
        svgEl.setAttribute('viewBox', '0 0 300 300');
        svgEl.setAttribute('width', width);
        svgEl.setAttribute('height', height);
        rootEl.append(svgEl);

        var lineEl = document.createElementNS(svgNs, 'line');
        lineEl.setAttribute('id', 'lineEl');
        lineEl.setAttribute('x1', coords[0]);
        lineEl.setAttribute('y1', coords[1]);
        lineEl.setAttribute('x2', coords[2]);
        lineEl.setAttribute('y2', coords[3]);
        lineEl.setAttribute('stroke', '#ff0000');
        svgEl.appendChild(lineEl);
    }
});

Скрипт для исправления проблемы:

01 
02 
03 
04 
05 
06 
07 
08 
09 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
48 
49 
50 
51 
52 
53 
54 
55 
56 
57 
58 
59 
60 
61 
62 
63 
64 
65 
66 
67 
68 
69 
70 
71 
72 
73 
74 
75 
76 
77 
78 
79 
80 
81 
82 
83 
84 
85 
86 
87 
88 
89 
90 
91 
var webkitSvgFix = (function() {

    var
        /**
         * Коэффициент изменения размеров.
         */
        factor = 1,
        /**
         * Элемент-родитель для проверки.
         * @type {jQuery}
         */
        rootEl,
        /**
         * SVG-элемент для проверки.
         * @type {Element}
         */
        svgEl,
        /**
         * Привязанные к изменению коэффициента обработчики.
         */
        funcs = [],
        svgNs = 'http://www.w3.org/2000/svg',
        /**
         * Интервал мониторинга, в мс.
         */
        timeout = 100;

    /**
     * Следит за размерами контрольных блоков.
     * При изменении коэффициента запускаются подписанные функции.
     */
    function check() {

        // Вычисляем коэффициент.
        var newFactor = rootEl.width() / svgEl.clientWidth;

        // Если коэффициент изменился, запускаем обработчики.
        if (newFactor != factor) {
            factor = newFactor;

            $.each(funcs, function() {
                this(factor);
            });
        }

        // Мониторим дальше.
        setTimeout(check, timeout);
    }

    /**
     * Инициализирует элементы для проверки масштабирования страницы.
     */
    function init() {
        var
            width = 1000,
            height = 1;

        // Создаем дивчик такой, чтобы его никто не увидел.
        rootEl = $('<div></div>').css({
            height: height,
            left: -10000,
            margin: 0,
            padding: 0,
            position: 'absolute',
            top: -10000,
            visibility: 'hidden',
            width: width
        }).appendTo('body');

        // И кладем в него SVG-элемент.
        svgEl = document.createElementNS(svgNs, 'svg');
        svgEl.setAttribute('version', '1.1');
        svgEl.setAttribute('width', width);
        svgEl.setAttribute('height', height);
        svgEl.style.position = 'absolute';
        rootEl.append(svgEl);

        // Начинаем мониторинг.
        check();
    }

    return {
        bind: function(func) {
            if ($.browser.safari) {
                // Инициализация только по первому привязанному обработчику.
                rootEl || init();
                funcs.push(func);
            }
        }
    };
})();