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.
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 data commands themselves.
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.
For example, I am able to convert this rounded rectangle to a <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 arc commands yields amusing results:
The dotted line is the actual transformed rect, the green fill is my path.
Following is the code I have so far (slightly pared-down). I also have a test page where I'm testing various shapes. Please help me determine how to properly transform the elliptical arc
and various other bezier commands given an arbitrary transformation 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+','+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;
}
I have made a general SVG flattener flatten.js, that supports all shapes and path commands: https://gist.github.com/timo22345/9413158
Basic usage: 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.
What can be flattened: entire SVG document, individual shapes (path, circle, ellipse etc.) and groups. Nested groups are handled automatically.
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.
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);
There are also arguments 'toAbsolute' (convert coordinates to absolute) and 'dec', number of digits after decimal separator.
Extreme path and shape tester: https://jsfiddle.net/fjm9423q/embedded/result/
Basic usage example: http://jsfiddle.net/nrjvmqur/embedded/result/
CONS: text element is not working. It could be my next goal.