I'm making a graph and chart web app and I want to perform the rotation operation on different shapes in it. Not using CSS or any transform attribute, I want to apply rotation on the path data (d parameter) itself. So, how can I mathematically process the points in the "d" attribute (both capital and small latter commands) so that It output a new "d" string representing the rotated shape after theta degree rotation about the origin (x,y)? For example, in this path string, d="M349,228h212v-1331h-212v133", I want to apply transform=rotate(24) and transform-origin="455px 161.5px". What will be the final d after I apply these transformations?
CodePudding user response:
To transform points in 2D space, use the following formulae, where the rotation direction is counter-clockwise and the angles are in radians.
rotatedX = xcosθ - ysinθ rotatedY = xsinθ ycosθ
CodePudding user response:
Another approach might be to transform path coordiates via matrixTransform()
.
This concept is used in timo22345's gist flatten.js and can also convert other transformations such as scale()
or translate()
to hardcoded path commands.
Simplified example
/**
* getTransformToElement polyfill
*/
SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function(toElement) {
let matrix = toElement.getScreenCTM().inverse().multiply(this.getScreenCTM())
return matrix;
};
function convertTransform(decimals = 3) {
let svg = document.querySelector('svg');
let transformed = svg.querySelectorAll('*[transform]');
let transformedTotal = transformed.length;
if (transformedTotal) {
transformed.forEach(function(el, i) {
let type = el.nodeName.toLowerCase();
let shapes = ['path', 'circle', 'rect', 'polygon', 'polyline', 'line', 'ellipse'];
if (shapes.indexOf(type) !== -1) {
// normalize pathdata to get absolute coordinates
let pathData = el.getPathData({
normalize: true
})
// get transform matrix
let matrix = el.getTransformToElement(el.parentNode);
el.removeAttribute('transform');
pathData.forEach(function(command, d) {
let values = command.values;
// loop through coordinates:
for (let v = 0; v < values.length - 1; v = 2) {
let [x, y] = [values[v], values[v 1]];
let pt = svg.createSVGPoint();
pt.x = x;
pt.y = y;
// change coordinates by matrix transform
let pTrans = pt.matrixTransform(matrix);
// save coordinates to pathdata array
pathData[d]['values'][v] = pTrans.x.toFixed(decimals);
pathData[d]['values'][v 1] = pTrans.y.toFixed(decimals);
}
})
//check if conversion is needed for primitives (rect, circle, polygons etc.)
if (type !== 'path') {
let newPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
let atts = [...el.attributes];
let excludedAtts = ['d', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'points', 'height', 'width'];
for (let a = 0; a < atts.length; a ) {
let att = atts[a];
if (excludedAtts.indexOf(att.nodeName) === -1) {
var attrName = att.nodeName;
var attrValue = att.nodeValue;
newPath.setAttribute(attrName, attrValue '');
}
}
el.replaceWith(newPath);
el = newPath;
}
el.setPathData(pathData)
}
})
}
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/path-data-polyfill.min.js"></script>
<p><button type="button" onclick="convertTransform()"> Convert transforms </button></p>
<svg viewBox="0 0 100 100" overflow="visible">
<path d="M25 25 h25 v25 h-25 z" fill="green" />
<rect x="0" y="0" width="33%" height="20" transform="rotate(27 20 50) translate(20 10)" fill="orange" />
<path data-att="random" transform="rotate(45 50 50) scale(1.2) translate(10 -20)" d="M25 25 h25 v25 h-25 z" fill="red" />
</svg>
How it works
- we need to get each element's transform matrix via
getTransformToElement()
(polyfill included) - we need to convert path data to absolute commands (using Jarek Foksa's path-data-polyfill)
- each command's point (x/y) can be transformed to new x/y values using
point.matrixTransform(matrix)
The above script is simplified: it can't convert nested transformations (like nested transformed groups) – timo22345 flatten.js can do this trick by applying multiple conversions.