Home > Back-end >  How to perform rotation in SVG path data (d) string itself?
How to perform rotation in SVG path data (d) string itself?

Time:07-13

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

  1. we need to get each element's transform matrix via getTransformToElement() (polyfill included)
  2. we need to convert path data to absolute commands (using Jarek Foksa's path-data-polyfill)
  3. 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.

  • Related