Javascript 烘焙转换为SVG路径元素命令
tl;dr summary:请提供资源或帮助修复以下代码,以通过任意矩阵转换SVGJavascript 烘焙转换为SVG路径元素命令,javascript,svg,2d,transform,Javascript,Svg,2d,Transform,tl;dr summary:请提供资源或帮助修复以下代码,以通过任意矩阵转换SVG元素的路径命令 详细信息: 我正在编写一个库来将任意SVG形状转换为元素。当层次结构中没有transform=“…”元素时,我可以使用它,但现在我想将对象的局部变换烘焙到命令本身中 在处理简单的moveto/lineto命令时,这主要起作用(下面的代码)。但是,我不确定转换bezier控制柄或arcTo参数的适当方法 例如,我可以将这个圆角矩形转换为: 这是一个更新的日志,记录了我正在取得的任何进展,作为“回答”,
元素的路径命令
详细信息:我正在编写一个库来将任意SVG形状转换为
元素。当层次结构中没有transform=“…”
元素时,我可以使用它,但现在我想将对象的局部变换烘焙到命令本身中
在处理简单的moveto/lineto命令时,这主要起作用(下面的代码)。但是,我不确定转换bezier控制柄或arcTo参数的适当方法
例如,我可以将这个圆角矩形转换为
:
这是一个更新的日志,记录了我正在取得的任何进展,作为“回答”,以帮助通知其他人;如果我能自己解决这个问题,我会接受的
更新1:我的命令运行良好,除了在非均匀比例的情况下。以下是补充内容:
// 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;
}
多亏了一种比我使用的更简单的提取方法,以及提取非均匀比例的数学。只要将所有坐标转换为绝对坐标,所有贝塞尔函数都可以正常工作;他们的把手没有什么神奇之处。对于椭圆弧命令,唯一的一般解决方案(如您所指出的,处理非均匀缩放,在一般情况下,弧命令无法表示)是首先将它们转换为bézier近似值 (在同一个文件中使用
absolutionizepath
作为黑客的直接端口)执行前者,但尚未执行后者
链接将圆弧转换为贝塞尔曲线的数学(每个0<α一个贝塞尔曲线段,如果每个对象(圆等)首先转换为路径,那么考虑变换就相当容易了。我制作了一个试验台()您可以在其中测试功能。测试台创建随机路径命令,并将随机变换应用于路径,然后展平变换。当然,在现实中,路径命令和变换不是随机的,但为了测试准确性,这是可以的
有一个函数flatte_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);
}, []);
});
老例子:
我创建了一个带有段mqaaqm
的路径,该路径应用了转换。该路径位于也应用了转换的g内部。为了确保该g位于另一个应用了不同转换的g内部。代码可以:
A) 首先对所有路径段进行规范化(多亏了我制作的Raphaël的path2curve,在此修复之后,所有可能的路径段组合最终都能正常工作:。正如您所看到的,原始Raphaël 2.1.0存在错误行为,如果没有,请多次单击路径以生成新曲线。)
B) 然后使用本机函数getTransformToElement()
、createSVGPoint()
和matrixTransform()
展平转换
唯一缺少的是将圆、矩形和多边形转换为路径命令的方法,但据我所知,您有一个很好的代码。我制作了一个通用SVG展平器flatten.js,它支持所有形状和路径命令:
基本用法:flant(document.getElementById('svg'))
功能:展平元素(将元素转换为路径并展平变换)。
如果参数元素(其id在“svg”之上)有子元素,或者其子元素有子元素,
这些子元素也被展平
可以展平的内容:整个SVG文档、单个形状(路径、圆、椭圆等)和组。嵌套组将自动处理
属性呢?将复制所有属性。只删除在path元素中无效的参数(例如r、rx、ry、cx、cy),但不再需要它们。此外,变换属性也会被删除,因为变换会展平到路径命令
如果要使用非仿射方法(例如透视扭曲)修改路径坐标,
可以使用以下方法将所有线段转换为三次曲线:
flant(document.getElementById('svg'),true)代码>
还有参数“toAbsolute”(将坐标转换为绝对坐标)和“dec”,
小数点分隔符后的位数
极限路径和形状测试仪:
基本用法示例:
缺点:文本元素不工作。这可能是我的下一个目标。对于好奇的人来说,这个库的动机是因为我真的想把每一个对象都变成一个模型,这样我就可以对它们进行操作。如果你不介意站在巨人的肩膀上,你当然可以重用德米特里·巴拉诺夫斯基的(麻省理工学院授权的),而不是自己重新实现它,这样:这是一个非常酷的测试平台,它演示了问题的另一个特性:如果路径不仅使用填充,而且使用笔划,那么复制原始外观就更加复杂,因为笔划路径实际上是一个轮廓形状,带有一个体积,由其笔划特性派生而来(宽度、线头和其他我可能忘记的)。给定一个倾斜或剪切变换,您实际上还必须导出轮廓的路径,将所有变换烘焙到轮廓中,并在没有笔划的情况下渲染轮廓,如果原始曲线有笔划,则在其填充曲线的顶部使用原始曲线笔划的填充颜色。但是,如果您也要冒险解决该问题,请做出另一个答案,而不是重新绘制他的第三次——这很可能会让答案很难理解,尽管很复杂,但大多数时候你想要的只是一个将所有缩放、旋转和平移应用到一个好的路径中的技巧,上面已经做了大量的工作。在我的测试床上,路径笔划不会被转换。如果它们必须接受的话为了说明这一点,AFAIK笔划必须转换为路径。这同样适用于文本、文本笔划和所有其他o
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;
}
// 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;
}
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);
}, []);
});
// 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;
}