烘焙转换为 SVG 路径元素命令

IT技术 javascript svg 2d transform
2021-02-08 23:47:06

tl;dr 摘要:给我资源或帮助修复以下代码,以<path>通过任意矩阵转换 SVG元素的路径命令

详细信息
我正在编写一个库来将任何任意 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;
}
4个回答

我制作了一个通用的 SVG flattener flatten.js,支持所有形状和路径命令:https ://gist.github.com/timo22345/9413158

基本用法:flatten(document.getElementById('svg'));

它的作用:展平元素(将元素转换为路径并展平变换)。如果参数元素(其 id 在 'svg' 之上)有子元素,或者它的后代有子元素,这些子元素也会被扁平化。

可以展平的内容:整个 SVG 文档、单个形状(路径、圆形、椭圆等)和组。嵌套组会自动处理。

属性呢?复制所有属性。仅删除在路径元素中无效的参数(例如 r、rx、ry、cx、cy),但不再需要它们。还删除了转换属性,因为转换被扁平化为路径命令。

如果您想使用非仿射方法(例如透视扭曲)修改路径坐标,您可以使用以下方法将所有线段转换为三次曲线: flatten(document.getElementById('svg'), true);

还有参数“toAbsolute”(将坐标转换为绝对坐标)和“dec”,小数点分隔符后的位数。

极限路径和形状测试器:https : //jsfiddle.net/fjm9423q/embedded/result/

基本使用示例:http : //jsfiddle.net/nrjvmqur/embedded/result/

缺点:文本元素不起作用。这可能是我的下一个目标。

谢谢,@Phrogz。Chrome 放弃了对 SVGElement.prototype.getTransformToElement 的支持。我更新了示例以使用垫片。
2021-03-14 23:47:06
你能提供一个链接到计算扁平路径的算法吗?
2021-03-25 23:47:06
此答案似乎不再适用于 Windows 上的 Chrome v50.0.2661.102。
2021-04-07 23:47:06
当你的函数没有返回值时,你应该如何使用它?!
2021-04-08 23:47:06

如果每个对象(圆圈等)都首先转换为路径,那么考虑转换就很容易了。我制作了一个测试平台 ( 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() 是错误修正版本。

如果使用参数 normalize_path=true 调用 flatten_transformations(),则所有命令都转换为 Cubics,一切正常。并且可以通过删除if (letter == "A") { ... }和删除 H、V 和 Z 的处理来简化代码。简化版本可以是这样的

但是因为有人可能只想烘焙转换而不是进行 All Segs -> Cubics 规范化,所以我添加了一种可能性。因此,如果您想使用 normalize_path=false 来展平变换,这意味着椭圆弧参数也必须展平,并且不可能通过简单地将矩阵应用于坐标来处理它们。两个半径 (rx ry)、x 轴旋转、大弧标志和扫掠标志必须分别处理。所以下面的函数可以展平弧的变换。矩阵参数是一个关系矩阵,它来自已经在 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 中,该 g 应用了不同的转换。代码可以:

A)首先将所有路径段归一化(感谢 Raphaël 的 path2curve,我对此进行了错误修复,在此修复之后,所有可能的路径段组合终于起作用了:http : //jsbin.com/oqojan/42。原始的 Raphaël 2.1 0.0有错误行为,你可以看到这里,如果不点击路径几次,以产生新的曲线。)

B) 然后使用本机函数getTransformToElement(),createSVGPoint()来展平转换matrixTransform()

唯一缺少的是将圆形、矩形和多边形转换为路径命令的方法,但据我所知,你有一个很好的代码。

但是,如果您也尝试解决该问题,请做另一个答案,而不是第三次重新调整这个答案 - 这很可能会使答案非常难以阅读,尽管如此复杂,但大多数时候您想要的是只是一个将所有缩放、旋转和平移应用到一个不错的路径中的 hack,上面已经做得很好了。
2021-03-21 23:47:06
这是一个非常酷的测试平台,它展示了问题的另一个属性:如果您的路径不仅使用填充而且使用笔画,那么重现原始外观仍然更加复杂,因为笔画路径实际上是一个带有体积的轮廓形状,源自其笔触属性(宽度、线帽以及我忘记的其他属性)。给定倾斜或剪切变换,您实际上还导出了轮廓的路径,将所有变换烘焙到其中,并在没有描边的情况下进行渲染,并在其填充曲线的顶部使用来自原始曲线的描边的填充颜色(如果有的话) .
2021-03-27 23:47:06
在我的测试平台路径笔画没有被转换。如果他们必须考虑,AFAIK 笔画必须转换为路径。这同样适用于文本、文本笔画和除路径及其笔画之外的所有其他对象。这肯定是可能的,几乎。只有字体很难,因为SVG不支持任何字体(=机器字体)的路径提取。
2021-03-27 23:47:06

这是我作为“答案”所做的任何前进进展的更新日志,以帮助通知其他人;如果我以某种方式自己解决问题,我会接受这一点。

更新 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;
}

感谢这个答案提供了比我使用的更简单的提取方法,以及用于提取非均匀比例的数学。

@allenhwkim 不,我没有取得比这里和我的网站上所代表的更多的进步。
2021-03-15 23:47:06

只要将所有坐标转换为绝对坐标,所有贝塞尔曲线都可以正常工作;他们的手柄没有什么神奇之处。至于椭圆弧命令,唯一的通用解决方案(处理非均匀缩放,正如您所指出的,在一般情况下,弧命令不能表示)是首先将它们转换为它们的贝塞尔近似值。

https://github.com/johan/svg-js-utils/blob/df605f3e21cc7fcd2d604eb318fb2466fd6d63a7/paths.js#L56..L113absolutizePath在同一个文件中使用,你的Convert SVG Path to Absolute 命令的直接端口)执行命令前者,但还不是后者。

如何用贝塞尔曲线最好地近似几何弧?链接将弧转换为贝塞尔曲线的数学运算(每个0 < α <= π/2弧段一个贝塞尔曲线段);本文在页面末尾显示了方程式(其更漂亮的pdf格式在第 3.4.1 节的末尾)。

如果你不介意站在巨人的肩膀上,你当然可以重用 Dmitry Baranovskiy 的(MIT 许可)Raphael.path2curve而不是自己重新实现它,像这样:github.com/johan/svg-js-utils/commit /…
2021-03-16 23:47:06