javascript 烘焙转换为 SVG 路径元素命令
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/5149301/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Baking transforms into SVG Path Element commands
提问by Phrogz
tl;dr summary: Give me the resources or help fix the below code to transform path commands for SVG <path>elements by an arbitrary matrix.
tl;dr 摘要:给我资源或帮助修复以下代码,以<path>通过任意矩阵转换 SVG元素的路径命令。
details:
I'm writing a library to convert any arbitrary SVG shape into a <path>element. I have it working when there are no transform="..."elements in the hierarchy, but now I want to bake the local transform of the object into the path datacommands themselves.
详细信息:
我正在编写一个库来将任何任意 SVG 形状转换为<path>元素。当transform="..."层次结构中没有元素时,我让它工作,但现在我想将对象的本地转换烘焙到路径数据命令本身。
This is mostly working (code below)when dealing with the simple moveto/lineto commands. However, I'm not sure of the appropriate way to transform the bezier handles or arcTo parameters.
在处理简单的 moveto/lineto 命令时,这主要是有效的(下面的代码)。但是,我不确定转换贝塞尔曲线句柄或 arcTo 参数的适当方法。
For example, I am able to convert this rounded rectangle to a <path>:
例如,我可以将这个圆角矩形转换为<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" />
And I get a valid result when transforming without any round corners:
在没有任何圆角的情况下进行转换时,我得到了一个有效的结果:
<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" />
However, transforming only the x/y coords of the elliptical arccommands yields amusing results:

The dotted line is the actual transformed rect, the green fill is my path.
然而,仅变换椭圆弧命令的 x/y 坐标会产生有趣的结果:

虚线是实际变换的矩形,绿色填充是我的路径。
Following is the code I have so far (slightly pared-down). I also have a test pagewhere I'm testing various shapes. Please help me determine how to properly transform the elliptical arcand various other bezier commands given an arbitrary transformation matrix.
以下是我到目前为止的代码(略有缩减)。我还有一个测试页面,我正在测试各种形状。请帮助我确定如何在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;
}
回答by Timo K?hk?nen
I have made a general SVG flattener flatten.js, that supports all shapes and path commands: https://gist.github.com/timo22345/9413158
我制作了一个通用的 SVG flattener flatten.js,支持所有形状和路径命令:https://gist.github.com/timo22345/9413158
Basic usage: flatten(document.getElementById('svg'));
基本用法:flatten(document.getElementById('svg'));
What it does: Flattens elements (converts elements to paths and flattens transformations). If the argument element (whose id is above 'svg') has children, or it's descendants has children, these children elements are flattened also.
它的作用:展平元素(将元素转换为路径并展平变换)。如果参数元素(其 id 在 'svg' 之上)有子元素,或者它的后代有子元素,这些子元素也会被扁平化。
What can be flattened: entire SVG document, individual shapes (path, circle, ellipse etc.) and groups. Nested groups are handled automatically.
可以展平的内容:整个 SVG 文档、单个形状(路径、圆形、椭圆等)和组。嵌套组会自动处理。
How about attributes? All attributes are copied. Only arguments that are not valid in path element, are dropped (eg. r, rx, ry, cx, cy), but they are not needed anymore. Also transform attribute is dropped, because transformations are flattened to path commands.
属性呢?复制所有属性。仅删除在路径元素中无效的参数(例如 r、rx、ry、cx、cy),但不再需要它们。还删除了转换属性,因为转换被扁平化为路径命令。
If you want to modify path coordinates using non-affine methods (eg. perspective distort),
you can convert all segments to cubic curves using:
flatten(document.getElementById('svg'), true);
如果您想使用非仿射方法(例如透视扭曲)修改路径坐标,您可以使用以下方法将所有线段转换为三次曲线:
flatten(document.getElementById('svg'), true);
There are also arguments 'toAbsolute' (convert coordinates to absolute) and 'dec', number of digits after decimal separator.
还有参数“toAbsolute”(将坐标转换为绝对坐标)和“dec”,小数点分隔符后的位数。
Extreme path and shape tester: https://jsfiddle.net/fjm9423q/embedded/result/
极限路径和形状测试器:https: //jsfiddle.net/fjm9423q/embedded/result/
Basic usage example: http://jsfiddle.net/nrjvmqur/embedded/result/
基本使用示例:http: //jsfiddle.net/nrjvmqur/embedded/result/
CONS: text element is not working. It could be my next goal.
缺点:文本元素不起作用。这可能是我的下一个目标。
回答by Timo K?hk?nen
If every object (circles etc) are converted first to paths, then taking transforms into account is rather easy. I made a testbed ( http://jsbin.com/oqojan/73) where you can test the functionality. The testbed creates random path commands and applies random transforms to paths and then flattens transforms. Of course in reality the path commands and transforms are not random, but for testing accuracy it is fine.
如果每个对象(圆圈等)都首先转换为路径,那么考虑转换就很容易了。我制作了一个测试平台 ( http://jsbin.com/oqojan/73),您可以在其中测试功能。测试平台创建随机路径命令并将随机变换应用于路径,然后展平变换。当然,实际上路径命令和变换不是随机的,但是为了测试准确性,它很好。
There is a function flatten_transformations(), which makes the main task:
有一个函数 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, "").replace(/\s*([-])/gi, "");
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);
}, []);
});
The code uses Raphael.pathToRelative(), Raphael._pathToAbsolute() and Raphael.path2curve(). The Raphael.path2curve() is bugfixed version.
代码使用 Raphael.pathToRelative()、Raphael._pathToAbsolute() 和 Raphael.path2curve()。Raphael.path2curve() 是错误修正版本。
If flatten_transformations() is called using argument normalize_path=true, then all commands are converted to Cubics and everything is fine. And the code can be simplified by removing if (letter == "A") { ... }and also removing handling of H, V and Z. The simplified version can be something like this.
如果使用参数 normalize_path=true 调用 flatten_transformations(),则所有命令都转换为 Cubics,一切正常。并且可以通过删除if (letter == "A") { ... }和删除 H、V 和 Z 的处理来简化代码。简化版本可以是这样的。
But because someone may want to only bake transformations and not to make All Segs -> Cubics normalization, I added there a possibility to this. So, if you want to flatten transformations with normalize_path=false, this means that Elliptical Arc parameters have to be flattened also and it's not possible to handle them by simply applying matrix to coordinates. Two radiis (rx ry), x-axis-rotation, large-arc-flag and sweep-flag have to handle separately. So the following function can flatten transformations of Arcs. The matrix parameter is a relation matrix which comes from is used already in flatten_transformations().
但是因为有人可能只想烘焙转换而不是进行 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;
}
OLD EXAMPLE:
旧示例:
I made an examplethat has a path with segments M Q A A Q M, which has transformations applied. The path is inside g that also has trans applied. And to make very sure this g is inside another g which has different transformations applied. And the code can:
我做了一个例子,它有一个带有段的路径M Q A A Q M,它应用了转换。路径在 g 内,也应用了 trans。并确保这个 g 在另一个 g 中,该 g 应用了不同的转换。代码可以:
A) First normalize those all path segments (thanks to Rapha?l's path2curve, to which I made a bug fix, and after this fix all possible path segment combinations worked finally: http://jsbin.com/oqojan/42. The original Rapha?l 2.1.0 has buggy behavior as you can see here, if not click paths few times to generate new curves.)
A)首先对所有路径段进行标准化(感谢 Rapha?l 的 path2curve,对此我进行了错误修复,在此修复之后,所有可能的路径段组合终于起作用了:http: //jsbin.com/oqojan/42。原始Rapha?l 2.1.0 有错误的行为,你可以在这里看到,如果不是点击路径几次以生成新曲线。)
B) Then flatten transformations using native functions getTransformToElement(), createSVGPoint()and matrixTransform().
B) 然后使用本机函数getTransformToElement(),createSVGPoint()和来展平转换matrixTransform()。
The only one that lacks is the way to convert Circles, Rectangles and Polygons to path commands, but as far as I know, you have an excellent code for it.
唯一缺少的是将圆形、矩形和多边形转换为路径命令的方法,但据我所知,你有一个很好的代码。
回答by ecmanaut
As long as you translate all coordinates to absolute coordinates, all béziers will work just fine; there is nothing magical about the their handles. As for the elliptical arc commands, the only general solution (handling non-uniform scaling, as you point out, which the arc command can not represent, in the general case) is to first convert them to their bézier approximations.
只要将所有坐标转换为绝对坐标,所有贝塞尔曲线都可以正常工作;他们的手柄没有什么神奇之处。至于椭圆弧命令,唯一的通用解决方案(处理非均匀缩放,如您所指出的,在一般情况下,弧命令不能表示)是首先将它们转换为它们的贝塞尔近似值。
https://github.com/johan/svg-js-utils/blob/df605f3e21cc7fcd2d604eb318fb2466fd6d63a7/paths.js#L56..L113(uses absolutizePathin the same file, a straight port of your Convert SVG Path to Absolute Commandshack) does the former, but not yet the latter.
https://github.com/johan/svg-js-utils/blob/df605f3e21cc7fcd2d604eb318fb2466fd6d63a7/paths.js#L56..L113(absolutizePath在同一个文件中使用,你的Convert SVG Path to Absolute 命令的直接端口)执行命令前者,但还不是后者。
How to best approximate a geometrical arc with a Bezier curve?links the math for converting arcs to béziers (one bézier segment per 0 < α <= π/2arc segment); this papershows the equations at the end of the page (its prettier pdfrendition has it at the end of section 3.4.1).
如何用贝塞尔曲线最好地近似几何弧?链接将弧转换为贝塞尔曲线的数学运算(每个0 < α <= π/2弧段一个贝塞尔曲线段);本文在页面末尾显示了方程式(其更漂亮的pdf格式在第 3.4.1 节的末尾)。
回答by Phrogz
This is an updated log of any forward progress I am making as an 'answer', to help inform others; if I somehow solve the problem on my own, I'll just accept this.
这是我作为“答案”所做的任何前进进展的更新日志,以帮助通知其他人;如果我以某种方式自己解决问题,我会接受这一点。
Update 1: I've got the absolute arctocommand working perfectly except in cases of non-uniform scale. Here were the additions:
更新 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;
}
Thanks to this answerfor a simpler extraction method than I was using, and for the math for extracting non-uniform scale.
感谢这个答案提供了比我使用的更简单的提取方法,以及用于提取非均匀比例的数学。

