Выпечка превращается в SVG Path Element команды - PullRequest
21 голосов
/ 01 марта 2011

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

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

Это в основномработа (код ниже) при работе с простыми командами 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 эллиптической дуги дает неожиданные результаты: Rounded rectangle with green blobs oozing from the corners outside the boundary
Пунктирная линия - это фактически преобразованный прямоугольник, зеленая заливка - мой путь.

Ниже приведен код, который у меня есть (слегка урезанный).У меня также есть тестовая страница , где я тестирую различные формы.Пожалуйста, помогите мне определить, как правильно преобразовать 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;
}

Ответы [ 4 ]

15 голосов
/ 07 марта 2014

Я сделал общий SVG flattener 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/

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

4 голосов
/ 27 октября 2012

Если каждый объект (круги и т. Д.) Сначала преобразуется в контуры, то учесть преобразования довольно просто.Я сделал тестовую площадку (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. Упрощенная версия может быть чем-то вроде this .

Но поскольку кто-то может захотеть толькозапекать преобразования и не делать нормализацию всех сегов -> кубиков, я добавил туда возможность к этому.Таким образом, если вы хотите сгладить преобразования с помощью 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) Сначала нормализовать все эти сегменты пути (благодаря pathhacurve Рафаэля, для которого я сделал исправление ошибки , и после этого исправления все возможные комбинации сегментов пути сработали окончательно: http://jsbin.com/oqojan/42. Исходный Raphaël 2.1.0 имеет некорректное поведение, как вы можете видеть здесь , если не нажимать пути несколько раз для создания новых кривых.)

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

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

2 голосов
/ 09 июля 2012

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

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

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

1 голос
/ 01 марта 2011

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

Обновление 1 : у меня работает команда absolute 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;
}

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

...