Получить глобальную матрицу преобразования элемента svg

Я хотел бы использовать браузер svg + JavaScript для создания динамических изображений из подготовленных пользователем шаблонов svg (скажем, созданных в Inkscape), где есть прямоугольные заполнители, которые отмечают место, где должна быть размещена динамическая графика.

В идеале пользователям — создателям шаблонов — должно быть разрешено перемещать, масштабировать, вращать, наклонять прямоугольники (не столько прямоугольники после наклона), сколько они хотят. Единственное, что они должны сделать, это установить правильное значение идентификатора для этих прямоугольников-заполнителей.

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


person Passiday    schedule 12.02.2016    source источник


Ответы (1)


Команды SVG getCTM и/или getScreenCTM могут быть тем, что вам нужно. (Я полагаю, что CTM означает «Текущая матрица преобразования».) Вы можете использовать их, например, извлекая элемент с помощью jQuery, удаляя окружающий объект jQuery и вызывая команду, например. $("#mySvgCircleId")[0].getScreenCTM(). Оба они возвращают матричные объекты SVG. Оба предоставят вам матричную информацию, которая учитывает их собственные прямые преобразования, а также любые преобразования, которые были применены к любым родительским элементам svg, в которые вложены фигуры. Это похоже на то, что вы искали.

Обратите внимание, однако, что между этими двумя командами есть важные различия. Я демонстрирую некоторые из этих различий в приведенном ниже фрагменте кода. Там я показываю два элемента svg, один с двумя красными прямоугольниками и один с двумя синими. Все прямоугольники имеют одинаковую ширину и высоту, но один прямоугольник каждого цвета остается непреобразованным, а другой вложен в три группы, преобразованные по-разному. Выходные данные показывают результаты матрицы, используя getCTM и getScreenCTM для каждого прямоугольника. Обратите внимание на следующее:

  • И getCTM, и getScreenCTM учитывают преобразования любых вмещающих элементов-предков, например. любые элементы группы g, вплоть до окружающего элемента svg. например Возвращаемые матрицы различаются для «untransformedRect1» и «transformedRect1» для обеих команд.
  • На результаты getCTM не влияет расположение окружающего элемента svg в его родительском элементе, в то время как на результаты getScreenCTM влияет. например Результаты для 'untransformedRect1' и 'untransformedRect2' идентичны при использовании getCTM, но различаются при использовании getScreenCTM.

Могут быть дополнительные сложности, если, например, вы имеете дело с iframe, вложением элементов svg в другие элементы svg и т. д., которые я здесь не рассматриваю.

var infoType = "CTM";
show("Matrix Results from getCTM()");
show(getInfo(infoType, "untransformedRect1"));
show(getInfo(infoType, "transformedRect1"));
show(getInfo(infoType, "untransformedRect2"));
show(getInfo(infoType, "transformedRect2"));
show("<br />");

var infoType = "ScreenCTM";
show("Matrix Results from getScreenCTM()");
show(getInfo(infoType, "untransformedRect1"));
show(getInfo(infoType, "transformedRect1"));
show(getInfo(infoType, "untransformedRect2"));
show(getInfo(infoType, "transformedRect2"));

function getInfo(mtrx, id) {
  var mtrx;
  if (infoType === "CTM") {
    var mtrx = $("#" + id)[0].getCTM();
  } else if (infoType === "ScreenCTM") {
    var mtrx = $("#" + id)[0].getScreenCTM();
  }
  var str =
    r(mtrx.a) + ",  " +
    r(mtrx.b) + ",  " +
    r(mtrx.c) + ",  " +
    r(mtrx.d) + ",  " +
    r(mtrx.e) + ",  " +
    r(mtrx.f);
  return id + ": matrix: " + str;
}

function r(num) {
  return Math.round(num * 1000) / 1000;
}

function show(msg) {
  document.write(msg + "<br />");
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>Depending on how you are viewing this, you may need to scroll down to see the matrix values.</p>
<div id="containerForSvgs">
  <svg id="svg1" width="150" height="100">
    <rect id="background1" width="300" height="200" fill="#eee" transform="translate(0,0)"></rect>
    <rect id="untransformedRect1" width="20" height="10" fill="red"></rect>
    <g id="group1A" transform="scale(2)">
      <g id="group1B" transform="translate(40,20)">
        <g id="group1C" transform="rotate(-15)">
          <rect id="transformedRect1" width="20" height="10" fill="red"></rect>
        </g>
      </g>
    </g>
  </svg>
  <br />
  shift ===>
  <svg id="svg2" width="150" height="100">
    <rect id="background2" width="300" height="200" fill="#eee" transform="translate(0,0)"></rect>
    <rect id="untransformedRect2" width="20" height="10" fill="blue"></rect>
    <g id="group2A" transform="scale(2)">
      <g id="group2B" transform="translate(40,20)">
        <g id="group2C" transform="rotate(-15)">
          <rect id="transformedRect2" width="20" height="10" fill="blue"></rect>
        </g>
      </g>
    </g>
  </svg>
</div>
<br />

ОБНОВЛЕНИЕ На самом деле, исследуя это, я немного углубился в спецификации SVG и обнаружил очень классную другую функцию, которая может оказаться еще более мощной для того, что вам нужно: getTransformToElement. По сути, вы можете получить кумулятивную матрицу преобразования из любого элемента (который я назову target) в любой из включающих его элементов (который я назову enclosing) с помощью одной команды: target.getTransformToElement(enclosing).

Ниже я предоставил еще один фрагмент кода, демонстрирующий его поведение, с именованным идентификатором, который, надеюсь, прояснит, как он может иметь отношение к вашей ситуации. Фрагмент показывает, что target.getCTM() по существу обеспечивает тот же результат, что и target.getTransformToElement(enclosingSvgElement). Кроме того, однако, он также показывает, что он более гибкий, поскольку может отображать преобразование от под-под-вложенного элемента к любому из его предков-охватывающих элементов на любом произвольном расстоянии. Более того, вы можете смотреть в любом направлении, например. как target.getTransformToElement(enclosing), так и enclosing.getTransformToElement(target), где одна будет матрицей, обратной другой (если я правильно понимаю свою математическую терминологию).

var svg  = $("svg"                                            )[0];
var grp1 = $("#grp1_formatting_of_entire_app"                 )[0];
var grp2 = $("#grp2_menus_and_buttons_and_stuff"              )[0];
var grp3 = $("#grp3_main_drawing_canvas"                      )[0];
var grp4 = $("#grp4_some_intervening_group"                   )[0];
var shp1 = $("#shp1_the_shape_I_currently_care_about"         )[0];
var grp5 = $("#grp5_a_lower_group_I_dont_currently_care_about")[0];

var shp1_CTM     = shp1.getCTM();
var shp1_to_svg  = shp1.getTransformToElement(svg);
var shp1_to_grp3 = shp1.getTransformToElement(grp3);
var grp3_to_shp1 = grp3.getTransformToElement(shp1);

document.write("<table>");

show("getCTM for shp1"                                 , shp1_CTM    );
show("getTransformToElement from shp1 to enclosing svg", shp1_to_svg );
show("getTransformToElement from shp1 to grp3"         , shp1_to_grp3);
show("getTransformToElement from grp3 to shp1"         , grp3_to_shp1);

document.write("</table>");


function show(msg, mtrx) {
  document.write("<tr><td>" + msg + "</td><td>" + mtrxStr(mtrx) + "</td></tr>");
}

function mtrxStr(mtrx) {
  return "( " +
    rnd(mtrx.a) + ", " +
    rnd(mtrx.b) + ", " +
    rnd(mtrx.c) + ", " +
    rnd(mtrx.d) + ", " +
    rnd(mtrx.e) + ", " +
    rnd(mtrx.f) + " )";
}

function rnd(n) {
  return Math.round(n*10)/10;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg width="200" height="60">
  <g            id="grp1_formatting_of_entire_app"                  transform="translate(30.0, 0)">
    <g          id="grp2_menus_and_buttons_and_stuff"               transform="translate(10.0, 0)">
      <g        id="grp3_main_drawing_canvas"                       transform="translate( 3.0, 0)">
        <g      id="grp4_some_intervening_group"                    transform="translate( 1.0, 0)">
          <rect id="shp1_the_shape_I_currently_care_about"          transform="translate( 0.3, 0)"
                 x="0" y="0" width="100" height="40" fill="red"></rect>
          <g    id="grp5_a_lower_group_I_dont_currently_care_about" transform="translate( 0.1, 0)">
          </g>
        </g>
      </g>
    </g>
  </g>
</svg>
<p>Results</p>

person Andrew Willems    schedule 13.02.2016
comment
К сожалению, Chrome больше не поддерживает эту замечательную функцию: chromestatus.com/feature/5736166087196672. - person Passiday; 15.02.2016
comment
Это очень плохо, потому что я был очень рад найти это. Это значительно упростило бы манипуляции с сильно сгруппированным изображением svg. Ну что ж. Большое спасибо за информацию. - person Andrew Willems; 15.02.2016
comment
Вы можете использовать полифилл для браузеров, которые не поддерживают эту функцию (глупый хром...) SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || функция (toElement) { return toElement.getScreenCTM().inverse().multiply(this.getScreenCTM()); }; - person Jason Crist; 26.05.2016
comment
@JasonCrist, это отличный полифилл, спасибо! Я беспокоюсь, однако. Полифиллы часто существуют для функций, которые еще не реализованы конкретным браузером. Здесь речь идет о функции, которая уже была реализована, а затем активно удалена. Представляет ли это закулисные разговоры, которые являются частью более широкой тенденции удаления этой функции из всех браузеров? Если это так, то в какой-то момент такие полифиллы могут стать тяжелыми битвами, в которые не стоит вступать. Однако, если хотя бы getScreenCTM останется, ваше решение по-прежнему определенно ценно. - person Andrew Willems; 27.05.2016