2011-03-01 73 views
21

tl; dr汇总:给我资源或帮助修复以下代码,以便通过任意矩阵转换SVG <path>元素的路径命令。烘烤变换为SVG路径元素命令

细节
我正在写一个库为任意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" /> 

然而,只有转化elliptical arc命令的x/y坐标产生有趣的结果: Rounded rectangle with green blobs oozing from the corners outside the boundary
虚线是th e实际转换矩形,绿色填充是我的路径。

以下是代码我迄今(略有删节过)。我也有一个test page我正在测试各种形状。请帮助我确定如何正确转换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; 
} 
+1

对于好奇,这个库的动机是因为我实际上想要将每个对象变成[采样点的多边形](http://phrogz.net/SVG/convert_path_to_polygon.xhtml),以便我可以执行[复平面非仿射变换](http://phrogz.net/SVG/transforming_paths.xhtml)。 – Phrogz 2011-03-01 00:57:07

回答

1

这是我提出作为一个“答案”,以帮助他人告知任何前瞻性进度更新的日志;如果我以某种方式自己解决问题,我会接受这一点。

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

感谢this answer一个简单的提取方法比我所用,并为数学用于提取非均匀缩放。

+0

我喜欢你的简单代码比拉斐尔的。这事有进一步更新吗? – allenhwkim 2014-11-09 22:17:01

+0

@allenhwkim不,我还没有取得比这里和我的网站上所呈现的更多的进展。 – Phrogz 2014-11-10 01:36:37

2

只要你把所有坐标绝对坐标,所有贝济会工作得很好;他们的手柄没有什么神奇的。至于椭圆弧命令,唯一的通用解决方案(处理非均匀缩放,正如您所指出的那样,在一般情况下弧线命令无法表示)首先将它们转换为它们的贝塞尔近似值。

https://github.com/johan/svg-js-utils/blob/df605f3e21cc7fcd2d604eb318fb2466fd6d63a7/paths.js#L56..L113(在同一文件中使用absolutizePath,您的Convert SVG Path to Absolute Commands黑客的直接端口)是前者,但还不是后者。

How to best approximate a geometrical arc with a Bezier curve?将弧转换成贝塞尔曲线的数学运算(每弧段0 < α <= π/2一个bézier弧段); this paper显示页面末尾处的公式(其更漂亮的pdf翻译在3.4.1节的结尾处)。

+1

如果你不介意站在巨人的肩膀上,你当然可以重用Dmitry Baranovskiy's(MIT许可)[Raphael.path2curve](http://raphaeljs.com/reference.html#Raphael.path2curve),而不是自己重新实现它,像这样:https://github.com/johan/svg-js-utils/commit/ec55fda7c41a5d19b2d399fdd955b4b5c42ba8ae – ecmanaut 2012-07-09 04:02:18

4

如果每个对象(圆圈等)都首先转换为路径,那么考虑转换是相当容易的。我做了一个测试平台(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的处理来简化。简化版本可以是this之类的东西。

但是因为有人可能只想烘烤转换而不是使所有分段 - >立方体标准化,所以我增加了这种可能性。所以,如果你想用normalize_path = false来变换变换,这意味着椭圆弧参数也必须被平滑,并且不可能通过简单地将矩阵应用于坐标来处理它们。两个radiis(rx ry),x轴旋转,大弧标志和扫描标志必须分开处理。所以下面的函数可以平滑Arcs的转换。矩阵参数是来自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; 
} 

OLD实施例:

我提出an example具有与段M Q A A Q M一个路径,其具有施加变换。路径在g内,也有trans应用。并且要确定这个g是在另一个具有不同转换的g中。和代码可:

A)首先正常化的所有路段(感谢拉斐尔的path2curve,而我做a bug fix,并在此之后解决所有可能的路径段组合终于达成:http://jsbin.com/oqojan/42原来拉斐尔2.1.0有错误行为,你可以看到here,如果不点击路径几次,以产生新的曲线。)

B)然后拼合使用本机的功能getTransformToElement()createSVGPoint()matrixTransform()转换。

唯一缺乏的是将圆,矩形和多边形转换为路径命令的方式,但据我所知,你有一个优秀的代码。

+0

这是一个非常酷的测试床,它展示了问题的另一个属性:如果你的路径不仅仅是填充而是还有笔画,因为描边的路径实际上是一个带有音量的轮廓形状,从它的笔触属性(宽度,线条和其他我忘记的)衍生而来,它仍然更复杂。 给定一个歪斜或剪切变换,实际上也可以派生轮廓的路径将所有变换烘焙到其中,并且在没有笔画的情况下渲染该轮廓,并在原始曲线的笔画上填充颜色,如果它在其填充曲线的顶部有一个。 – ecmanaut 2012-11-04 21:38:15

+0

但是,如果你冒险去解决这个问题,那么做另一个答案,而不是第三次重试这个问题 - 这很可能会让答案很难阅读,因为所有的复杂性,当大多数时候你想要的东西只是一种将所有缩放,旋转和平移应用到一个不错的路径中的破解,上面已经做了很好的工作。 – ecmanaut 2012-11-04 21:40:26

+0

在我的测试平台中,笔画不会被转换。如果他们必须考虑到,AFAIK笔画必须转换为路径。这同样适用于文本,文本笔划以及除路径和笔画之外的所有其他对象。它几乎是可能的。只有字体很难,因为SVG不支持任何字体(=机器字体)的路径提取。 – 2012-11-04 22:55:10

14

我制作了一般的SVG扁平器。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/

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

+0

此答案不再适用于Windows上的Chrome v50.0.2661.102。 – Phrogz 2016-05-23 14:50:08

+0

谢谢@Phrogz。 Chrome放弃了对SVGElement.prototype.getTransformToElement的支持。我更新了示例以使用垫片。 – 2016-05-23 16:18:40