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?
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>