Выпечка превращается в SVG Path Element команды

Резюме: предоставьте мне ресурсы или помогите исправить приведенный ниже код для преобразования команд пути для SVG <path> элементы произвольной матрицей.

детали:
Я пишу библиотеку для преобразования любой произвольной формы SVG в <path> элемент. У меня это работает когда нет transform="..." элементы в иерархии, но теперь я хочу испечь локальное преобразование объекта в самих данных команд пути.

Это в основном работает (код ниже) при работе с простыми командами moveto/lineto. Однако я не уверен в правильном способе преобразования маркеров Безье или параметров arcTo.

Например, я могу преобразовать этот закругленный прямоугольник в <path>:

<rect x="10" y="30" rx="10" ry="20" width="80" height="70" />
--> <path d=​"M20,30 L80,30 A10,20,0,0,1,90,50 L90,80 A10,20,0,0,1,80,100
             L20,100 A10,20,0,0,1,10,80 L10,50 A10,20,0,0,1,20,30" />

И я получаю действительный результат при преобразовании без каких-либо закругленных углов:

<rect x="10" y="30" width="80" height="70"
      transform="translate(-200,0) scale(1.5) rotate(50)" />
--> <path d=​"M10,30 L90,30 L90,100 L10,100 L10,30" />

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

Ниже приведен код, который у меня есть (слегка урезанный). У меня также есть тестовая страница, где я тестирую различные формы. Пожалуйста, помогите мне определить, как правильно преобразовать elliptical arc и различные другие команды Безье, заданные произвольной матрицей преобразования.

function flattenToPaths(el,transform,svg){
  if (!svg) svg=el; while(svg && svg.tagName!='svg') svg=svg.parentNode;
  var doc = el.ownerDocument;
  var svgNS = svg.getAttribute('xmlns');

  // Identity transform if nothing passed in
  if (!transform) transform= svg.createSVGMatrix();

  // Calculate local transform matrix for the object
  var localMatrix = svg.createSVGMatrix();
  for (var xs=el.transform.baseVal,i=xs.numberOfItems-1;i>=0;--i){
    localMatrix = xs.getItem(i).matrix.multiply(localMatrix);
  }
  // Transform the local transform by whatever was recursively passed in
  transform = transform.multiply(localMatrix);

  var path = doc.createElementNS(svgNS,'path');
  switch(el.tagName){
    case 'rect':
      path.setAttribute('stroke',el.getAttribute('stroke'));
      var x  = el.getAttribute('x')*1,     y  = el.getAttribute('y')*1,
          w  = el.getAttribute('width')*1, h  = el.getAttribute('height')*1,
          rx = el.getAttribute('rx')*1,    ry = el.getAttribute('ry')*1;
      if (rx && !el.hasAttribute('ry')) ry=rx;
      else if (ry && !el.hasAttribute('rx')) rx=ry;
      if (rx>w/2) rx=w/2;
      if (ry>h/2) ry=h/2;
      path.setAttribute('d',
        'M'+(x+rx)+','+y+
        'L'+(x+w-rx)+','+y+
        ((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+w)+','+(y+ry)) : '') +
        'L'+(x+w)+','+(y+h-ry)+
        ((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+w-rx)+','+(y+h)) : '')+
        'L'+(x+rx)+','+(y+h)+
        ((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+x+','+(y+h-ry)) : '')+
        'L'+x+','+(y+ry)+
        ((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+rx)+','+y) : '')
      );
    break;

    case 'circle':
      var cx = el.getAttribute('cx')*1, cy = el.getAttribute('cy')*1,
          r  = el.getAttribute('r')*1,  r0 = r/2+','+r/2;
      path.setAttribute('d','M'+cx+','+(cy-r)+' A'+r0+',0,0,0,'+cx+','+(cy+r)+' '+r0+',0,0,0,'+cx+','+(cy-r) );
    break;

    case 'ellipse':
      var cx = el.getAttribute('cx')*1, cy = el.getAttribute('cy')*1,
          rx = el.getAttribute('rx')*1, ry = el.getAttribute('ry')*1;
      path.setAttribute('d','M'+cx+','+(cy-ry)+' A'+rx+','+ry+',0,0,0,'+cx+','+(cy+ry)+' '+rx+','+ry+',0,0,0,'+cx+','+(cy-ry) );
    break;

    case 'line':
      var x1=el.getAttribute('x1')*1, y1=el.getAttribute('y1')*1,
          x2=el.getAttribute('x2')*1, y2=el.getAttribute('y2')*1;
      path.setAttribute('d','M'+x1+','+y1+'L'+x2+','+y2);
    break;

    case 'polyline':
    case 'polygon':
      for (var i=0,l=[],pts=el.points,len=pts.numberOfItems;i<len;++i){
        var p = pts.getItem(i);
        l[i] = p.x+','+p.y;
      }
      path.setAttribute('d',"M"+l.shift()+"L"+l.join(' ') + (el.tagName=='polygon') ? 'z' : '');
    break;

    case 'path':
      path = el.cloneNode(false);
    break;
  }

  // Convert local space by the transform matrix
  var x,y;
  var pt = svg.createSVGPoint();
  var setXY = function(x,y,xN,yN){
    pt.x = x; pt.y = y;
    pt = pt.matrixTransform(transform);
    if (xN) seg[xN] = pt.x;
    if (yN) seg[yN] = pt.y;
  };

  // Extract rotation and scale from the transform
  var rotation = Math.atan2(transform.b,transform.d)*180/Math.PI;
  var sx = Math.sqrt(transform.a*transform.a+transform.c*transform.c);
  var sy = Math.sqrt(transform.b*transform.b+transform.d*transform.d);

  // FIXME: Must translate any Horizontal or Vertical lineto commands into absolute moveto
  for (var segs=path.pathSegList,c=segs.numberOfItems,i=0;i<c;++i){
    var seg = segs.getItem(i);

    // Odd-numbered path segments are all relative
    // http://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg
    var isRelative = (seg.pathSegType%2==1);
    var hasX = seg.x != null;
    var hasY = seg.y != null;
    if (hasX) x = isRelative ? x+seg.x : seg.x;
    if (hasY) y = isRelative ? y+seg.y : seg.y;
    if (hasX || hasY) setXY( x, y, hasX && 'x', hasY && 'y' );

    if (seg.x1 != null) setXY( seg.x1, seg.y1, 'x1', 'y1' );
    if (seg.x2 != null) setXY( seg.x2, seg.y2, 'x2', 'y2' );
    if (seg.angle != null){
      seg.angle += rotation;
      seg.r1 *= sx; // FIXME; only works for uniform scale
      seg.r2 *= sy; // FIXME; only works for uniform scale
    }
  }

  return path;
}

5 ответов

Я сделал общий SVG-корректор flatten.js, который поддерживает все формы и команды пути: https://gist.github.com/timo22345/9413158

Основное использование: flatten(document.getElementById('svg'));

Что он делает: Сглаживает элементы (преобразует элементы в контуры и сглаживает преобразования). Если у элемента аргумента (идентификатор которого выше 'svg') есть дочерние элементы, или у его потомков есть дочерние элементы, эти дочерние элементы также сглаживаются.

Что может быть сведено: весь документ SVG, отдельные фигуры (контур, круг, эллипс и т. Д.) И группы. Вложенные группы обрабатываются автоматически.

Как насчет атрибутов? Все атрибуты скопированы. Только аргументы, которые недопустимы в элементе пути, удаляются (например, r, rx, ry, cx, cy), но они больше не нужны. Также атрибут transform отбрасывается, потому что преобразования сглаживаются по командам пути.

Если вы хотите изменить координаты пути, используя неаффинные методы (например, искажение перспективы), вы можете преобразовать все сегменты в кубические кривые, используя:flatten(document.getElementById('svg'), true);

Есть также аргументы 'toAbsolute' (преобразовать координаты в абсолютные) и 'dec', количество цифр после десятичного разделителя.

Тестер экстремальных путей и форм: https://jsfiddle.net/fjm9423q/embedded/result/

Пример базового использования: http://jsfiddle.net/nrjvmqur/embedded/result/

Минусы: текстовый элемент не работает. Это может быть моей следующей целью.

Если каждый объект (круги и т. Д.) Сначала преобразуется в контуры, то учесть преобразования довольно просто. Я сделал тестовую площадку ( http://jsbin.com/oqojan/73), где вы можете проверить функциональность. Стенд создает команды произвольного пути и применяет случайные преобразования к путям, а затем выравнивает преобразования. Конечно, в действительности команды пути и преобразования не случайны, но для проверки точности это хорошо.

Есть функция flatten_transformations(), которая выполняет основную задачу:

function flatten_transformations(path_elem, normalize_path, to_relative, dec) {

    // Rounding coordinates to dec decimals
    if (dec || dec === 0) {
        if (dec > 15) dec = 15;
        else if (dec < 0) dec = 0;
    }
    else dec = false;

    function r(num) {
        if (dec !== false) return Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
        else return num;
    }

    // For arc parameter rounding
    var arc_dec = (dec !== false) ? 6 : false;
    arc_dec = (dec && dec > 6) ? dec : arc_dec;

    function ra(num) {
        if (arc_dec !== false) return Math.round(num * Math.pow(10, arc_dec)) / Math.pow(10, arc_dec);
        else return num;
    }

    var arr;
    //var pathDOM = path_elem.node;
    var pathDOM = path_elem;
    var d = pathDOM.getAttribute("d").trim();

    // If you want to retain current path commans, set normalize_path to false
    if (!normalize_path) { // Set to false to prevent possible re-normalization. 
        arr = Raphael.parsePathString(d); // str to array
        arr = Raphael._pathToAbsolute(arr); // mahvstcsqz -> uppercase
    }
    // If you want to modify path data using nonAffine methods,
    // set normalize_path to true
    else arr = Raphael.path2curve(d); // mahvstcsqz -> MC
    var svgDOM = pathDOM.ownerSVGElement;

    // Get the relation matrix that converts path coordinates
    // to SVGroot's coordinate space
    var matrix = pathDOM.getTransformToElement(svgDOM);

    // The following code can bake transformations
    // both normalized and non-normalized data
    // Coordinates have to be Absolute in the following
    var i = 0,
        j, m = arr.length,
        letter = "",
        x = 0,
        y = 0,
        point, newcoords = [],
        pt = svgDOM.createSVGPoint(),
        subpath_start = {};
    subpath_start.x = "";
    subpath_start.y = "";
    for (; i < m; i++) {
        letter = arr[i][0].toUpperCase();
        newcoords[i] = [];
        newcoords[i][0] = arr[i][0];

        if (letter == "A") {
            x = arr[i][6];
            y = arr[i][7];

            pt.x = arr[i][6];
            pt.y = arr[i][7];
            newcoords[i] = arc_transform(arr[i][4], arr[i][5], arr[i][6], arr[i][4], arr[i][5], pt, matrix);
            // rounding arc parameters
            // x,y are rounded normally
            // other parameters at least to 5 decimals
            // because they affect more than x,y rounding
            newcoords[i][7] = ra(newcoords[i][8]); //rx
            newcoords[i][9] = ra(newcoords[i][10]); //ry
            newcoords[i][11] = ra(newcoords[i][12]); //x-axis-rotation
            newcoords[i][6] = r(newcoords[i][6]); //x
            newcoords[i][7] = r(newcoords[i][7]); //y
        }
        else if (letter != "Z") {
            // parse other segs than Z and A
            for (j = 1; j < arr[i].length; j = j + 2) {
                if (letter == "V") y = arr[i][j];
                else if (letter == "H") x = arr[i][j];
                else {
                    x = arr[i][j];
                    y = arr[i][j + 1];
                }
                pt.x = x;
                pt.y = y;
                point = pt.matrixTransform(matrix);
                newcoords[i][j] = r(point.x);
                newcoords[i][j + 1] = r(point.y);
            }
        }
        if ((letter != "Z" && subpath_start.x == "") || letter == "M") {
            subpath_start.x = x;
            subpath_start.y = y;
        }
        if (letter == "Z") {
            x = subpath_start.x;
            y = subpath_start.y;
        }
        if (letter == "V" || letter == "H") newcoords[i][0] = "L";
    }
    if (to_relative) newcoords = Raphael.pathToRelative(newcoords);
    newcoords = newcoords.flatten().join(" ").replace(/\s*([A-Z])\s*/gi, "$1").replace(/\s*([-])/gi, "$1");
    return newcoords;
} // function flatten_transformations​​​​​

// Helper tool to piece together Raphael's paths into strings again
Array.prototype.flatten || (Array.prototype.flatten = function() {
  return this.reduce(function(a, b) {
      return a.concat('function' === typeof b.flatten ? b.flatten() : b);
    }, []);
});

Код использует Raphael.pathToRelative(), Raphael._pathToAbsolute() и Raphael.path2curve(). Raphael.path2curve () является исправленной версией.

Если flatten_transformations () вызывается с использованием аргумента normalize_path=true, то все команды преобразуются в кубики, и все в порядке. И код может быть упрощен путем удаления if (letter == "A") { ... } а также удаление обработки H, V и Z. Упрощенная версия может быть примерно такой.

Но поскольку кто-то может захотеть только запекать преобразования, а не нормализовать все сегменты -> кубики, я добавил к этому возможность. Таким образом, если вы хотите сгладить преобразования с помощью normalize_path=false, это означает, что параметры эллиптической дуги также должны быть сглажены, и невозможно обработать их, просто применив матрицу к координатам. Два радиуса (rx ry), вращение по оси X, флаг большой дуги и флаг развертки должны обрабатываться отдельно. Таким образом, следующая функция может сгладить преобразования дуг. Параметр matrix- это матрица отношений, которая используется уже в flatten_transformations().

// Origin: http://devmaster.net/forums/topic/4947-transforming-an-ellipse/
function arc_transform(a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, endpoint, matrix, svgDOM) {
    function NEARZERO(B) {
        if (Math.abs(B) < 0.0000000000000001) return true;
        else return false;
    }

    var rh, rv, rot;

    var m = []; // matrix representation of transformed ellipse
    var s, c; // sin and cos helpers (the former offset rotation)
    var A, B, C; // ellipse implicit equation:
    var ac, A2, C2; // helpers for angle and halfaxis-extraction.
    rh = a_rh;
    rv = a_rv;

    a_offsetrot = a_offsetrot * (Math.PI / 180); // deg->rad
    rot = a_offsetrot;

    s = parseFloat(Math.sin(rot));
    c = parseFloat(Math.cos(rot));

    // build ellipse representation matrix (unit circle transformation).
    // the 2x2 matrix multiplication with the upper 2x2 of a_mat is inlined.
    m[0] = matrix.a * +rh * c + matrix.c * rh * s;
    m[1] = matrix.b * +rh * c + matrix.d * rh * s;
    m[2] = matrix.a * -rv * s + matrix.c * rv * c;
    m[3] = matrix.b * -rv * s + matrix.d * rv * c;

    // to implict equation (centered)
    A = (m[0] * m[0]) + (m[2] * m[2]);
    C = (m[1] * m[1]) + (m[3] * m[3]);
    B = (m[0] * m[1] + m[2] * m[3]) * 2.0;

    // precalculate distance A to C
    ac = A - C;

    // convert implicit equation to angle and halfaxis:
    if (NEARZERO(B)) {
        a_offsetrot = 0;
        A2 = A;
        C2 = C;
    } else {
        if (NEARZERO(ac)) {
            A2 = A + B * 0.5;
            C2 = A - B * 0.5;
            a_offsetrot = Math.PI / 4.0;
        } else {
            // Precalculate radical:
            var K = 1 + B * B / (ac * ac);

            // Clamp (precision issues might need this.. not likely, but better save than sorry)
            if (K < 0) K = 0;
            else K = Math.sqrt(K);

            A2 = 0.5 * (A + C + K * ac);
            C2 = 0.5 * (A + C - K * ac);
            a_offsetrot = 0.5 * Math.atan2(B, ac);
        }
    }

    // This can get slightly below zero due to rounding issues.
    // it's save to clamp to zero in this case (this yields a zero length halfaxis)
    if (A2 < 0) A2 = 0;
    else A2 = Math.sqrt(A2);
    if (C2 < 0) C2 = 0;
    else C2 = Math.sqrt(C2);

    // now A2 and C2 are half-axis:
    if (ac <= 0) {
        a_rv = A2;
        a_rh = C2;
    } else {
        a_rv = C2;
        a_rh = A2;
    }

    // If the transformation matrix contain a mirror-component 
    // winding order of the ellise needs to be changed.
    if ((matrix.a * matrix.d) - (matrix.b * matrix.c) < 0) {
        if (!sweep_flag) sweep_flag = 1;
        else sweep_flag = 0;
    }

    // Finally, transform arc endpoint. This takes care about the
    // translational part which we ignored at the whole math-showdown above.
    endpoint = endpoint.matrixTransform(matrix);

    // Radians back to degrees
    a_offsetrot = a_offsetrot * 180 / Math.PI;

    var r = ["A", a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, endpoint.x, endpoint.y];
    return r;
}

СТАРЫЙ ПРИМЕР:

Я сделал пример, который имеет путь с сегментами M Q A A Q M, который имеет примененные преобразования. Путь находится внутри g, к которому также применяется trans. И чтобы убедиться, что это g находится внутри другого g, к которому применены другие преобразования. И код может:

A) Сначала нормализуйте все эти сегменты пути (благодаря path2curve Рафаэля, в который я внес исправление ошибки, и после этого исправления все возможные комбинации сегментов пути сработали окончательно: http://jsbin.com/oqojan/42. Оригинальный Raphaël 2.1.0 имеет некорректное поведение, как вы можете видеть здесь, если не нажимать пути несколько раз, чтобы генерировать новые кривые.)

Б) Затем сгладить преобразования, используя нативные функции getTransformToElement(), createSVGPoint() а также matrixTransform(),

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

Пока вы переводите все координаты в абсолютные координаты, все безье будут работать нормально; нет ничего волшебного в их ручках. Что касается команд эллиптической дуги, единственное общее решение (обработка неравномерного масштабирования, как вы указали, которую команда дуги не может представлять в общем случае) - это сначала преобразовать их в их приближения Безье.

https://github.com/johan/svg-js-utils/blob/df605f3e21cc7fcd2d604eb318fb2466fd6d63a7/paths.js#L56..L113 (использует absolutizePath в том же файле прямой порт вашего хака Convert SVG Path to Absolute Commands выполняет первое, но еще не второе.

Как наилучшим образом приблизить геометрическую дугу к кривой Безье? связывает математику для преобразования дуг в Безье (один сегмент Безье на 0 < α <= π/2 сегмент дуги); эта статья показывает уравнения в конце страницы (в более красивой версии PDF она есть в конце раздела 3.4.1).

Это обновленный журнал любого прогресса, который я делаю в качестве "ответа", чтобы помочь информировать других; если я как-то решу проблему самостоятельно, я просто приму это.

Обновление 1: у меня абсолютная команда arcto работает отлично, за исключением случаев неравномерного масштаба. Здесь были дополнения:

// Extract rotation and scale from the transform
var rotation = Math.atan2(transform.b,transform.d)*180/Math.PI;
var sx = Math.sqrt(transform.a*transform.a+transform.c*transform.c);
var sy = Math.sqrt(transform.b*transform.b+transform.d*transform.d);

//inside the processing of segments
if (seg.angle != null){
  seg.angle += rotation;
  // FIXME; only works for uniform scale
  seg.r1 *= sx;
  seg.r2 *= sy;
}

Благодаря этому ответу для более простого метода извлечения, чем я использовал, и для математики для извлечения неравномерного масштаба.

Вдохновленный ответом Тимо Кяхконена и его Flatten.js , я написал аналогичный вспомогательный скрипт, используя для получения необходимых данных.сутью
полифил getpathData() Ярека Фоксы

Как это работает

  • преобразовать все элементы геометрии SVG в<path>. Позвонивelement.getpathData({normalize:true})мы получаем данные пути также для таких примитивов, как<rect>или<circle>.
    Кроме того, все команды преобразуются в абсолютные команды с использованием только кубических кривых Безье и линето. Arctos преобразуются в кубическую форму Безье.
  • получить общую матрицу преобразования для каждого элемента с учетом всех унаследованных преобразований (например, из родительских групп)
       let matrix = parent.getScreenCTM().inverse().multiply(el.getScreenCTM());

На самом деле это просто замена (к сожалению) устаревшегоgetTransformToElement()метод, который также может быть заполнен таким образом

      SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function(toElement) {
    return toElement.getScreenCTM().inverse().multiply(this.getScreenCTM());
};
  • пересчитать все координаты команды черезmatrixTransform()вот так
      let pt = svg.createSVGPoint();
pt.x = x;
pt.y = y;
let pTrans = pt.matrixTransform(matrix);
  • масштабировать ширину штрихов
  • удалить все атрибуты преобразования и свойства стиля для элементов геометрии и родительского элемента.<g>групповые элементы. Этот шаг следует делать последним — иначе мы не получим правильные значения матрицы преобразования для дочерних элементов.

«Детрансформация»<text>элементы?

Очевидно, что мы не можем конвертировать текстовые элементы в пути (если только мы не используем такую ​​библиотеку, как opentype.js или fontkit). Но мы можем объединить все преобразования, влияющие на текст, и применить автономное значение атрибута преобразования (как мы делали раньше для геометрических элементов).

я используюqrDecomposeMatrix()вспомогательная функция, основанная на великолепном ответе АндреаБогацци: «Найти вращение и наклон матричного преобразования», чтобы разделить текущую матрицу на отдельные функции преобразования, такие какtranslate(),scale()и т. д.

Для тестирования: пример Codepen

Другие вопросы по тегам