烘焙转换成SVGpath元素命令

tl; dr总结 :给我资源或帮助修复下面的代码,以便通过任意matrix转换SVG <path>元素的path命令。

细节
我在写一个库来将任意的SVG形状转换成一个<path>元素。 当它在层次结构中没有transform="..."元素的时候,我已经工作了,但是现在我想将对象的局部变换烘焙到path数据命令本身中。

在处理简单的moveto / lineto命令时,这主要是工作(下面的代码) 。 但是,我不确定转换贝塞尔句柄或arcTo参数的适当方式。

例如,我可以将这个圆angular矩形转换为<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" /> 

在没有任何圆angular的情况下,我得到了一个有效的结果:

 <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坐标产生有趣的结果: 圆形的矩形,从边界外的角落渗出绿色斑点
虚线是实际转换矩形,绿色填充是我的path。

以下是我到目前为止的代码(略微缩减)。 我也有一个testing页 ,我正在testing各种形状。 请帮助我确定如何正确转换elliptical arc和各种其他贝塞尔命令给定一个任意的转换matrix。

 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+','+py; } 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; } 

我做了一个普通的SVG flattenner flatten.js,它支持所有形状和path命令: https ://gist.github.com/timo22345/9413158

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

它做什么:压扁元素(将元素转换为path并展平变换)。 如果参数元素(其ID在“svg”之上)具有子元素,或者子元素具有子元素,则这些子元素也会变平。

什么可以被压扁:整个SVG文件,个别形状(path,圆形,椭圆等)和组。 嵌套组自动处理。

如何属性? 所有属性都被复制。 只有在path元素中无效的参数被丢弃(例如,r,rx,ry,cx,cy),但是它们不再需要。 此外,转换属性被丢弃,因为转换被平铺为path命令。

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

还有“toAbsolute”(将坐标转换为绝对值)和“dec”(十进制分隔符后的数字位数)的参数。

极限path和形状testing器: https : //jsfiddle.net/fjm9423q/embedded/result/

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

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

如果每个对象(圆圈等)首先转换为path,那么考虑转换是相当容易的。 我做了一个testing平台( http://jsbin.com/oqojan/73 ),您可以在其中testingfunction。 testing平台创build随机path命令,并将随机变换应用于path,然后平滑变换。 当然实际上path命令和变换不是随机的,但是为了testing的准确性,这是很好的。

有一个函数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*([AZ])\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的处理。简化的版本可以是这样的 。

但是因为有人可能只想烘烤转换,而不是使所有Segs – > Cubics正常化,所以我增加了一个可能性。 所以,如果你想用normalize_path = false来平滑变换,这意味着椭圆弧参数也必须被平滑,并且不可能通过简单地将matrix应用于坐标来处理它们。 两个radiis(rx ry),x轴旋转,大弧标志和扫描标志必须分别处理。 所以下面的函数可以平滑Arcs的转换。 matrix参数是来自flatten_transformations()的关系matrix。

 // 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; } 

老例子:

我做了一个示例 ,其中包含应用转换的段MQAAQM的path。 path在g也有trans应用。 而且要确定这个g是在另一个有不同转换的g中。 代码可以:

A)首先规范所有的path段(感谢Raphaël的path2曲线,我做了一个错误修复 ,并在此修复后,所有可能的path段组合终于工作: http : //jsbin.com/oqojan/42 。原来的Raphaël2.1 .0有bug的行为,你可以看到这里 ,如果不点击几次path生成新的曲线。)

B)然后使用本机函数getTransformToElement()createSVGPoint()matrixTransform()

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

只要你把所有的坐标转换成绝对坐标,所有的bézier都可以正常工作。 他们的手柄没有什么神奇的。 至于椭圆弧命令,唯一的一般解决scheme(处理非均匀缩放,正如你所指出的那样,在一般情况下,弧命令不能表示)是首先将它们转换成它们的贝塞尔近似。

https://github.com/johan/svg-js-utils/blob/df605f3e21cc7fcd2d604eb318fb2466fd6d63a7/paths.js#L56..L113 (在同一个文件中使用absolutizePath , 将SVGpath转换为绝对命令的直接端口)前者,但还不是后者。

如何用贝塞尔曲线最好地逼近几何弧? 将弧转换成贝塞尔曲线(每个0 < α <= π/2弧段一个贝塞尔曲线段); 本文将在页面末尾显示方程(其更漂亮的pdf格式在3.4.1节结尾处)。

这是一个更新的日志,logging了我作为“答案”所取得的任何进展,以帮助告知其他人; 如果我自己解决这个问题,我会接受这个。

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

由于这个答案比我使用的更简单的提取方法,以及提取非均匀比例的math。