3

So, let's say I have an SVG that looks like this:

<svg width="800" height="600" viewBox="0 0 800 600" style="border: 1px solid blue;">
 <path fill="#f00" stroke="none" d="M720 394.5c-27.98 0-51.61-6.96-71.97-18.72-29.64-17.1-52.36-44.37-71.48-75.12-28-45.01-48.31-97.48-71.39-136.52-20.03-33.88-42.14-57.64-73.16-57.64-31.1 0-53.24 23.88-73.31 57.89-23.04 39.05-43.34 91.45-71.31 136.39-19.28 30.98-42.21 58.41-72.2 75.45C195 387.72 171.62 394.5 144 394.5Z"/>
</svg>

As you can see, the path only occupies a portion of the SVG (and viewBox area).

I would like to know how to transform the values in the paths that they fill the viewBox (essentially rescaling and repositioning the values in the path so it fills the entire viewBox).

[UPDATE]

I'm adding a few more specifics...

Take an example - lets say that I'm starting with an SVG with a viewBox like this: 0 0 1600 1600.

In that SVG, there's a path that occupies the region from 1200,1200 to 1500,1400. (I.e., the path is 300 x 200).

I would like to be able to extract that path, and add it to a new SVG with a viewBox of 0 0 300 200.

To do this, the values in the d attribute need to be modified accordingly - essentially moved 1200 points up and to the left.

Obviously, absolute coordinates would need to change, but relative coordinates would not. (That should be pretty easy).

But I've also got to deal with curves and their control points, which might get a little tricky.

A perfect solution would be able to examine a path, identify the smallest bounding box that could contain it, then adjust all the points so they fit in that bounding box, anchored at 0,0.

I wouldn't want to scale or stretch the path.

I'm equally happy with a mathematical process or function to do this, or an online tool of some sort.

I realize that I can use an SVG transformation to accomplish this, but I want to be able to change the actual path.

(I.e., I don't want my web page to include "incorrect" data AND a transform to "correct" it; I just want my code to include the "correct" data.)

Is there a way to do this?

mattstuehler
  • 9,040
  • 18
  • 78
  • 108
  • 1
    [Inkscape](http://inkscape.org) can [freeze transforms](http://www.inkscapeforum.com/viewtopic.php?t=10205) – cxw Feb 10 '16 at 19:53
  • 1
    The other way around would probably be easier: Adapting the viewBox size to the path. For this you could look at: http://stackoverflow.com/q/16377186/1169798 – Sirko Feb 10 '16 at 20:03
  • @Sirko - that's a good idea, but then *everything else* in the SVG has to be relative to that adapted viewBox. What I'm really hoping to be able to do is take a path from another SVG (with an arbitrary viewBox), and adapt it to fit within a new SVG. So - I really need to be able to adapt the path, not the viewBox. – mattstuehler Feb 10 '16 at 20:07
  • 1
    You know, that you can nest SVG tags and thereby viewBoxes? – Sirko Feb 10 '16 at 20:09
  • @Sirko - that's another useful tip. But I'm trying to avoid making accommodations for data that's not scaled to the SVG. E.g., if I take a small path from the lower right corner of an enormous SVG (e.g., viewBox="0 0 1600 1600"), and use just that path in a small SVG (e.g., viewBox="0 0 100 100") - it then has a bunch of superfluous data. And it makes it much harder to maintain and debug. – mattstuehler Feb 10 '16 at 20:15
  • 1
    Then the next suggestion ,-): get the bounding box by the before link, see by which factor you have to scale it and then scale your path accordingly. maybe even using inkscape to freeze that scaling – Sirko Feb 10 '16 at 20:17
  • @Sirko - I'm not familiar with inkscape. But I'm definitely going to try it out to see if it can do this for me. Hopefully it's easy to use! – mattstuehler Feb 10 '16 at 20:19
  • If you don't have to do it at run-time, then use an SVG editor (like Inkscape, AI etc). If you **do** need to do it at run-time, then update your question with more info - such as how you want the scaling to work (stretch/keep aspect ratio etc). Also, did the new path fill the viewBox of the old SVG it came from? – Paul LeBeau Feb 11 '16 at 08:04
  • @Paul - Thanks for the tip. I don't have to do it at run-time, so I'm going to try Inkscape. I was just hoping there was an easy way to do it, or an online utility of some sort. I'm also updating my post to answer the questions you asked. – mattstuehler Feb 11 '16 at 19:54
  • Quote: `"Obviously, absolute coordinates would need to change, but relative coordinates would not. (That should be pretty easy). But I've also got to deal with curves and their control points, which might get a little tricky."` This was from the statement where you were talking about doing a `translate`, not a `scale`. The curves and control points in this case could be translated the same as all of the other points, just saying. – WebWanderer Feb 11 '16 at 20:11
  • @WebWanderer. True. I just meant that control points, radii, and relative coordinates make identifying the smallest possible bounding box a little trickier. – mattstuehler Feb 11 '16 at 20:16
  • Yes, especially when attempting to determine the height and width of you bezier curve, which is essentially what your are attempting to do in your example. What would be more difficult would be to determine to the farthest points of an abstract path, which makes my eyes spin. You've stumbled on a rough question my friend, but I actually hope someone solves this. This would be nice to have. I can look further into solving for your example, but I'm leaving abstract paths alone. – WebWanderer Feb 11 '16 at 20:22

2 Answers2

5

I had written most of my answer before you gave your update. Therefore my answer is a response to what I thought you originally wanted: to be able to directly change the "d" attribute of an SVG path so that the path now just fills the SVG viewport. Thus, my answer does involve scaling which you suggested you did want in your original answer but you didn't need in your update. In any case, I hope that my code gives you some idea of how to approach directly changing the d attribute without using a transform.

The code snippet below shows the original path you provided in red, with the "transformed" path shown in blue. Note in the svg code provided that the two paths start out identically. You can get the d attribute of the blue path, at least in Firefox, by right-clicking on the path and choosing "inspect element".

Hopefully the variable names and comments in the code provide the guidelines you need to understand my approach.

(Update: Fixed code in the code snippet so that it now also works in Chrome and Safari, not just in Firefox. It appears that some ES6 language features, e.g. "let", "const", destructuring, Symbols, work in Firefox but at least some of them don't work in Chrome or Safari. I haven't checked Internet Explorer or Opera or any other browsers.)

// Retrieve the "d" attribute of the SVG path you wish to transform.
var $svgRoot    = $("svg");
var $path       = $svgRoot.find("path#moved");
var oldPathDStr = $path.attr("d");

// Calculate the transformation required.
var obj = getTranslationAndScaling($svgRoot, $path);
var pathTranslX = obj.pathTranslX;
var pathTranslY = obj.pathTranslY;
var scale       = obj.scale;

// The path could be transformed at this point with a simple
// "transform" attribute as shown here.

// $path.attr("transform", `translate(${pathTranslX}, ${pathTranslY}), scale(${scale})`);

// However, as described in your question you didn't want this.
// Therefore, the code following this line mutates the actual svg path.

// Calculate the path "d" attributes parameters.
var newPathDStr = getTransformedPathDStr(oldPathDStr, pathTranslX, pathTranslY, scale);

// Apply the new "d" attribute to the path, transforming it.
$path.attr("d", newPathDStr);

document.write("<p>Altered 'd' attribute of path:</p><p>" + newPathDStr + "</p>");

// This is the end of the main code. Below are the functions called.



// Calculate the transformation, i.e. the translation and scaling, required
// to get the path to fill the svg area. Note that this assumes uniform
// scaling, a path that has no other transforms applied to it, and no
// differences between the svg viewport and viewBox dimensions.
function getTranslationAndScaling($svgRoot, $path) {
  var svgWdth = $svgRoot.attr("width" );
  var svgHght = $svgRoot.attr("height");

  var origPathBoundingBox = $path[0].getBBox();

  var origPathWdth = origPathBoundingBox.width ;
  var origPathHght = origPathBoundingBox.height;
  var origPathX    = origPathBoundingBox.x     ;
  var origPathY    = origPathBoundingBox.y     ;

  // how much bigger is the svg root element
  // relative to the path in each dimension?
  var scaleBasedOnWdth = svgWdth / origPathWdth;
  var scaleBasedOnHght = svgHght / origPathHght;

  // of the scaling factors determined in each dimension,
  // use the smaller one; otherwise portions of the path
  // will lie outside the viewport (correct term?)
  var scale = Math.min(scaleBasedOnWdth, scaleBasedOnHght);

  // calculate the bounding box parameters
  // after the path has been scaled relative to the origin
  // but before any subsequent translations have been applied

  var scaledPathX    = origPathX    * scale;
  var scaledPathY    = origPathY    * scale;
  var scaledPathWdth = origPathWdth * scale;
  var scaledPathHght = origPathHght * scale;

  // calculate the centre points of the scaled but untranslated path
  // as well as of the svg root element

  var scaledPathCentreX = scaledPathX + (scaledPathWdth / 2);
  var scaledPathCentreY = scaledPathY + (scaledPathHght / 2);
  var    svgRootCentreX = 0           + (svgWdth        / 2);
  var    svgRootCentreY = 0           + (svgHght        / 2);

  // calculate translation required to centre the path
  // on the svg root element

  var pathTranslX = svgRootCentreX - scaledPathCentreX;
  var pathTranslY = svgRootCentreY - scaledPathCentreY;

  return {pathTranslX, pathTranslY, scale};
}
  
function getTransformedPathDStr(oldPathDStr, pathTranslX, pathTranslY, scale) {

  // constants to help keep track of the types of SVG commands in the path
  var BOTH_X_AND_Y   = 1;
  var JUST_X         = 2;
  var JUST_Y         = 3;
  var NONE           = 4;
  var ELLIPTICAL_ARC = 5;
  var ABSOLUTE       = 6;
  var RELATIVE       = 7;

  // two parallel arrays, with each element being one component of the
  // "d" attribute of the SVG path, with one component being either
  // an instruction (e.g. "M" for moveto, etc.) or numerical value
  // for either an x or y coordinate
  var oldPathDArr = getArrayOfPathDComponents(oldPathDStr);
  var newPathDArr = [];

  var commandParams, absOrRel, oldPathDComp, newPathDComp;

  // element index
  var idx = 0;

  while (idx < oldPathDArr.length) {
    var oldPathDComp = oldPathDArr[idx];
    if (/^[A-Za-z]$/.test(oldPathDComp)) { // component is a single letter, i.e. an svg path command
      newPathDArr[idx] = oldPathDArr[idx];
      switch (oldPathDComp.toUpperCase()) {
        case "A": // elliptical arc command...the most complicated one
          commandParams = ELLIPTICAL_ARC;
          break;
        case "H": // horizontal line; requires only an x-coordinate
          commandParams = JUST_X;
          break;
        case "V": // vertical line; requires only a y-coordinate
          commandParams = JUST_Y;
          break;
        case "Z": // close the path
          commandParams = NONE;
          break;
        default: // all other commands; all of them require both x and y coordinates
          commandParams = BOTH_X_AND_Y;
      }
      absOrRel = ((oldPathDComp === oldPathDComp.toUpperCase()) ? ABSOLUTE : RELATIVE);
      // lowercase commands are relative, uppercase are absolute
      idx += 1;
    } else { // if the component is not a letter, then it is a numeric value
      var translX, translY;
      if (absOrRel === ABSOLUTE) { // the translation is required for absolute commands...
        translX = pathTranslX;
        translY = pathTranslY;
      } else if (absOrRel === RELATIVE) { // ...but not relative ones
        translX = 0;
        translY = 0;
      }
      switch (commandParams) {
        // figure out which of the numeric values following an svg command
        // are required, and then transform the numeric value(s) from the
        // original path d-attribute and place it in the same location in the
        // array that will eventually become the d-attribute for the new path
        case BOTH_X_AND_Y:
          newPathDArr[idx    ] = Number(oldPathDArr[idx    ]) * scale + translX;
          newPathDArr[idx + 1] = Number(oldPathDArr[idx + 1]) * scale + translY;
          idx += 2;
          break;
        case JUST_X:
          newPathDArr[idx    ] = Number(oldPathDArr[idx    ]) * scale + translX;
          idx += 1;
          break;
        case JUST_Y:
          newPathDArr[idx    ] = Number(oldPathDArr[idx    ]) * scale + translY;
          idx += 1;
          break;
        case ELLIPTICAL_ARC:
          // the elliptical arc has x and y values in the first and second as well as
          // the 6th and 7th positions following the command; the intervening values
          // are not affected by the transformation and so can simply be copied
          newPathDArr[idx    ] = Number(oldPathDArr[idx    ]) * scale + translX;
          newPathDArr[idx + 1] = Number(oldPathDArr[idx + 1]) * scale + translY;
          newPathDArr[idx + 2] = Number(oldPathDArr[idx + 2])                        ;
          newPathDArr[idx + 3] = Number(oldPathDArr[idx + 3])                        ;
          newPathDArr[idx + 4] = Number(oldPathDArr[idx + 4])                        ;
          newPathDArr[idx + 5] = Number(oldPathDArr[idx + 5]) * scale + translX;
          newPathDArr[idx + 6] = Number(oldPathDArr[idx + 6]) * scale + translY;
          idx += 7;
          break;
        case NONE:
          throw new Error('numeric value should not follow the SVG Z/z command');
          break;
      }
    }
  }
  return newPathDArr.join(" ");
}

function getArrayOfPathDComponents(str) {
  // assuming the string from the d-attribute of the path has all components
  // separated by a single space, then create an array of components by
  // simply splitting the string at those spaces
  str = standardizePathDStrFormat(str);
  return str.split(" ");
}

function standardizePathDStrFormat(str) {
  // The SVG standard is flexible with respect to how path d-strings are
  // formatted but this makes parsing them more difficult. This function ensures
  // that all SVG path d-string components (i.e. both commands and values) are
  // separated by a single space.
  return str
    .replace(/,/g         , " "   )  // replace each comma with a space
    .replace(/-/g         , " -"  )  // precede each minus sign with a space
    .replace(/([A-Za-z])/g, " $1 ")  // sandwich each   letter between 2 spaces
    .replace(/  /g        , " "   )  // collapse repeated spaces to a single space
    .replace(/ ([Ee]) /g  , "$1"  )  // remove flanking spaces around exponent symbols
    .replace(/^ /g        , ""    )  // trim any leading space
    .replace(/ $/g        , ""    ); // trim any tailing space
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg width="800" height="600" viewBox="0 0 800 600" style="border: 1px solid blue;">
 <path id="notmoved" fill="#f00" stroke="none" d="M720 394.5c-27.98 0-51.61-6.96-71.97-18.72-29.64-17.1-52.36-44.37-71.48-75.12-28-45.01-48.31-97.48-71.39-136.52-20.03-33.88-42.14-57.64-73.16-57.64-31.1 0-53.24 23.88-73.31 57.89-23.04 39.05-43.34 91.45-71.31 136.39-19.28 30.98-42.21 58.41-72.2 75.45C195 387.72 171.62 394.5 144 394.5Z" opacity="0.5" />
 <path id="moved" fill="#00f" stroke="none" d="M720 394.5c-27.98 0-51.61-6.96-71.97-18.72-29.64-17.1-52.36-44.37-71.48-75.12-28-45.01-48.31-97.48-71.39-136.52-20.03-33.88-42.14-57.64-73.16-57.64-31.1 0-53.24 23.88-73.31 57.89-23.04 39.05-43.34 91.45-71.31 136.39-19.28 30.98-42.21 58.41-72.2 75.45C195 387.72 171.62 394.5 144 394.5Z" opacity="0.5" />
</svg>
Andrew Willems
  • 11,880
  • 10
  • 53
  • 70
1

If you don't need to scale the new path, then all you need to do is apply a transform to move it over to the right place. If it starts at (1200, 1200), and you want it at (0,0), then make the transform "translate(-1200, -1200)"

<svg width="800" height="600" viewBox="0 0 800 600" style="border: 1px solid blue;">
    <path fill="#f00" stroke="none" transform="translate(-1200,-1200)"
          d="M720 394.5c-27.98 0-51.61-6.96-71.97-18.72-29.64-17.1-52.36-44.37-71.48-75.12-28-45.01-48.31-97.48-71.39-136.52-20.03-33.88-42.14-57.64-73.16-57.64-31.1 0-53.24 23.88-73.31 57.89-23.04 39.05-43.34 91.45-71.31 136.39-19.28 30.98-42.21 58.41-72.2 75.45C195 387.72 171.62 394.5 144 394.5Z"/>
</svg>
Paul LeBeau
  • 97,474
  • 9
  • 154
  • 181