Phr*_*ogz 22 javascript svg 2d transform
tl; dr summary:给我资源或帮助修复下面的代码<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" />
Run Code Online (Sandbox Code Playgroud)
在没有任何圆角的情况下进行转换时,我得到了有效的结果:
<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" />
Run Code Online (Sandbox Code Playgroud)
但是,仅转换椭圆弧命令的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;
}
Run Code Online (Sandbox Code Playgroud)
Tim*_*nen 16
我制作了一个通用的SVG flattener flatten.js,它支持所有形状和路径命令:https://gist.github.com/timo22345/9413158
基本用法:flatten(document.getElementById('svg'));
它的作用:展平元素(将元素转换为路径并展平变换).如果参数元素(其id高于'svg')具有子元素,或者它的子元素具有子元素,则这些子元素也会被展平.
什么可以展平:整个SVG文档,个别形状(路径,圆,椭圆等)和组.嵌套组自动处理.
属性怎么样?将复制所有属性.只删除在path元素中无效的参数(例如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/
缺点:文本元素不起作用.这可能是我的下一个目标.
如果每个对象(圆圈等)首先转换为路径,那么考虑变换就相当容易了。我制作了一个测试台(http://jsbin.com/oqojan/73),您可以在其中测试功能。测试床创建随机路径命令并对路径应用随机变换,然后展平变换。当然,实际上路径命令和变换不是随机的,但对于测试准确性来说这是可以的。
\n\n有一个函数 flatten_transformations(),它的主要任务是:
\n\nfunction flatten_transformations(path_elem, normalize_path, to_relative, dec) {\n\n // Rounding coordinates to dec decimals\n if (dec || dec === 0) {\n if (dec > 15) dec = 15;\n else if (dec < 0) dec = 0;\n }\n else dec = false;\n\n function r(num) {\n if (dec !== false) return Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);\n else return num;\n }\n\n // For arc parameter rounding\n var arc_dec = (dec !== false) ? 6 : false;\n arc_dec = (dec && dec > 6) ? dec : arc_dec;\n\n function ra(num) {\n if (arc_dec !== false) return Math.round(num * Math.pow(10, arc_dec)) / Math.pow(10, arc_dec);\n else return num;\n }\n\n var arr;\n //var pathDOM = path_elem.node;\n var pathDOM = path_elem;\n var d = pathDOM.getAttribute("d").trim();\n\n // If you want to retain current path commans, set normalize_path to false\n if (!normalize_path) { // Set to false to prevent possible re-normalization. \n arr = Raphael.parsePathString(d); // str to array\n arr = Raphael._pathToAbsolute(arr); // mahvstcsqz -> uppercase\n }\n // If you want to modify path data using nonAffine methods,\n // set normalize_path to true\n else arr = Raphael.path2curve(d); // mahvstcsqz -> MC\n var svgDOM = pathDOM.ownerSVGElement;\n\n // Get the relation matrix that converts path coordinates\n // to SVGroot\'s coordinate space\n var matrix = pathDOM.getTransformToElement(svgDOM);\n\n // The following code can bake transformations\n // both normalized and non-normalized data\n // Coordinates have to be Absolute in the following\n var i = 0,\n j, m = arr.length,\n letter = "",\n x = 0,\n y = 0,\n point, newcoords = [],\n pt = svgDOM.createSVGPoint(),\n subpath_start = {};\n subpath_start.x = "";\n subpath_start.y = "";\n for (; i < m; i++) {\n letter = arr[i][0].toUpperCase();\n newcoords[i] = [];\n newcoords[i][0] = arr[i][0];\n\n if (letter == "A") {\n x = arr[i][6];\n y = arr[i][7];\n\n pt.x = arr[i][6];\n pt.y = arr[i][7];\n newcoords[i] = arc_transform(arr[i][4], arr[i][5], arr[i][6], arr[i][4], arr[i][5], pt, matrix);\n // rounding arc parameters\n // x,y are rounded normally\n // other parameters at least to 5 decimals\n // because they affect more than x,y rounding\n newcoords[i][7] = ra(newcoords[i][8]); //rx\n newcoords[i][9] = ra(newcoords[i][10]); //ry\n newcoords[i][11] = ra(newcoords[i][12]); //x-axis-rotation\n newcoords[i][6] = r(newcoords[i][6]); //x\n newcoords[i][7] = r(newcoords[i][7]); //y\n }\n else if (letter != "Z") {\n // parse other segs than Z and A\n for (j = 1; j < arr[i].length; j = j + 2) {\n if (letter == "V") y = arr[i][j];\n else if (letter == "H") x = arr[i][j];\n else {\n x = arr[i][j];\n y = arr[i][j + 1];\n }\n pt.x = x;\n pt.y = y;\n point = pt.matrixTransform(matrix);\n newcoords[i][j] = r(point.x);\n newcoords[i][j + 1] = r(point.y);\n }\n }\n if ((letter != "Z" && subpath_start.x == "") || letter == "M") {\n subpath_start.x = x;\n subpath_start.y = y;\n }\n if (letter == "Z") {\n x = subpath_start.x;\n y = subpath_start.y;\n }\n if (letter == "V" || letter == "H") newcoords[i][0] = "L";\n }\n if (to_relative) newcoords = Raphael.pathToRelative(newcoords);\n newcoords = newcoords.flatten().join(" ").replace(/\\s*([A-Z])\\s*/gi, "$1").replace(/\\s*([-])/gi, "$1");\n return newcoords;\n} // function flatten_transformations\xe2\x80\x8b\xe2\x80\x8b\xe2\x80\x8b\xe2\x80\x8b\xe2\x80\x8b\n\n// Helper tool to piece together Raphael\'s paths into strings again\nArray.prototype.flatten || (Array.prototype.flatten = function() {\n return this.reduce(function(a, b) {\n return a.concat(\'function\' === typeof b.flatten ? b.flatten() : b);\n }, []);\n});\nRun Code Online (Sandbox Code Playgroud)\n\n该代码使用 Raphael.pathToRelative()、Raphael._pathToAbsolute() 和 Raphael.path2curve()。Raphael.path2curve() 是错误修复版本。
\n\n如果使用参数 normalize_path=true 调用 flatten_transformations(),则所有命令都将转换为 Cubics,并且一切正常。并且可以通过删除和删除 H、V 和 Z 的处理来简化代码。if (letter == "A") { ... }简化版本可以是这样的。
但是因为有人可能只想烘焙转换而不是使所有段 -> 三次标准化,所以我添加了这种可能性。因此,如果您想使用 normalize_path=false 来展平变换,这意味着椭圆弧参数也必须展平,并且不可能通过简单地将矩阵应用于坐标来处理它们。两个半径(rx ry)、x轴旋转、大弧标志和扫掠标志必须单独处理。所以下面的函数可以展平弧的变换。矩阵参数是一个关系矩阵,它已在 flatten_transformations() 中使用。
\n\n// Origin: http://devmaster.net/forums/topic/4947-transforming-an-ellipse/\nfunction arc_transform(a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, endpoint, matrix, svgDOM) {\n function NEARZERO(B) {\n if (Math.abs(B) < 0.0000000000000001) return true;\n else return false;\n }\n\n var rh, rv, rot;\n\n var m = []; // matrix representation of transformed ellipse\n var s, c; // sin and cos helpers (the former offset rotation)\n var A, B, C; // ellipse implicit equation:\n var ac, A2, C2; // helpers for angle and halfaxis-extraction.\n rh = a_rh;\n rv = a_rv;\n\n a_offsetrot = a_offsetrot * (Math.PI / 180); // deg->rad\n rot = a_offsetrot;\n\n s = parseFloat(Math.sin(rot));\n c = parseFloat(Math.cos(rot));\n\n // build ellipse representation matrix (unit circle transformation).\n // the 2x2 matrix multiplication with the upper 2x2 of a_mat is inlined.\n m[0] = matrix.a * +rh * c + matrix.c * rh * s;\n m[1] = matrix.b * +rh * c + matrix.d * rh * s;\n m[2] = matrix.a * -rv * s + matrix.c * rv * c;\n m[3] = matrix.b * -rv * s + matrix.d * rv * c;\n\n // to implict equation (centered)\n A = (m[0] * m[0]) + (m[2] * m[2]);\n C = (m[1] * m[1]) + (m[3] * m[3]);\n B = (m[0] * m[1] + m[2] * m[3]) * 2.0;\n\n // precalculate distance A to C\n ac = A - C;\n\n // convert implicit equation to angle and halfaxis:\n if (NEARZERO(B)) {\n a_offsetrot = 0;\n A2 = A;\n C2 = C;\n } else {\n if (NEARZERO(ac)) {\n A2 = A + B * 0.5;\n C2 = A - B * 0.5;\n a_offsetrot = Math.PI / 4.0;\n } else {\n // Precalculate radical:\n var K = 1 + B * B / (ac * ac);\n\n // Clamp (precision issues might need this.. not likely, but better save than sorry)\n if (K < 0) K = 0;\n else K = Math.sqrt(K);\n\n A2 = 0.5 * (A + C + K * ac);\n C2 = 0.5 * (A + C - K * ac);\n a_offsetrot = 0.5 * Math.atan2(B, ac);\n }\n }\n\n // This can get slightly below zero due to rounding issues.\n // it\'s save to clamp to zero in this case (this yields a zero length halfaxis)\n if (A2 < 0) A2 = 0;\n else A2 = Math.sqrt(A2);\n if (C2 < 0) C2 = 0;\n else C2 = Math.sqrt(C2);\n\n // now A2 and C2 are half-axis:\n if (ac <= 0) {\n a_rv = A2;\n a_rh = C2;\n } else {\n a_rv = C2;\n a_rh = A2;\n }\n\n // If the transformation matrix contain a mirror-component \n // winding order of the ellise needs to be changed.\n if ((matrix.a * matrix.d) - (matrix.b * matrix.c) < 0) {\n if (!sweep_flag) sweep_flag = 1;\n else sweep_flag = 0;\n }\n\n // Finally, transform arc endpoint. This takes care about the\n // translational part which we ignored at the whole math-showdown above.\n endpoint = endpoint.matrixTransform(matrix);\n\n // Radians back to degrees\n a_offsetrot = a_offsetrot * 180 / Math.PI;\n\n var r = ["A", a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, endpoint.x, endpoint.y];\n return r;\n}\nRun Code Online (Sandbox Code Playgroud)\n\n旧例子:
\n\n我制作了一个示例,其中包含包含段的路径M Q A A Q M,并且应用了转换。该路径位于 g 内部,也应用了反式。并确保这个 g 位于另一个应用了不同变换的 g 内。代码可以:
A)首先规范化所有路径段(感谢 Rapha\xc3\xabl\ 的 path2curve,我对此进行了错误修复,修复后所有可能的路径段组合最终起作用:http://jsbin.com/oqojan /42。原始 Rapha\xc3\xabl 2.1.0 存在错误行为,如您在此处看到的那样,如果没有单击路径几次以生成新曲线。)
\n\nB) 然后使用本机函数getTransformToElement()、createSVGPoint()和进行展平转换matrixTransform()。
唯一缺少的是将圆形、矩形和多边形转换为路径命令的方法,但据我所知,您有一个很好的代码。
\n这是我作为“答案”所做的任何进展的更新日志,以帮助通知其他人;如果我自己能以某种方式解决问题,我就会接受这一点。
更新 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;
}
Run Code Online (Sandbox Code Playgroud)
感谢这个答案提供了比我使用的更简单的提取方法,以及提取非均匀尺度的数学。
| 归档时间: |
|
| 查看次数: |
10608 次 |
| 最近记录: |