以数学方式转换 SVG 路径中的值以填充 viewBox

Mathematically transform the values in an SVG path to fill the viewBox

所以,假设我有一个如下所示的 SVG:

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

如你所见,路径只占据了SVG的一部分(和viewBox区域)。

我想知道如何转换它们填充 viewBox 的路径中的值(本质上是重新缩放和重新定位路径中的值,以便它填充整个 viewBox)。

[更新]

我正在添加更多细节...

举个例子 - 假设我开始使用带有 viewBox 的 SVG,如下所示:0 0 1600 1600.

在该 SVG 中,有一条路径占据了从 1200,12001500,1400 的区域。 (即路径为 300 x 200)。

我希望能够提取该路径,并将其添加到视图框为 0 0 300 200 的新 SVG 中。

为此,需要相应地修改 d 属性中的值 - 基本上向上和向左移动 1200 点。

显然,绝对坐标需要改变,但相对坐标则不需要。 (这应该很容易)。

但我还必须处理曲线及其控制点,这可能会有点棘手。

一个完美的解决方案将能够检查一条路径,确定可能包含它的最小边界框,然后调整所有点,使它们适合该边界框,锚定在 0,0

我不想缩放或拉伸路径。

我对执行此操作的数学过程或函数或某种在线工具同样满意。

我知道我可以使用 SVG 转换来完成此操作,但我希望能够更改实际路径。

(即,我不希望我的网页包含 "incorrect" 数据并转换为 "correct" 它;我只希望我的代码包含 "correct" 数据。 )

有办法吗?

如果您不需要缩放新路径,那么您需要做的就是应用 transform 将其移动到正确的位置。如果它开始于 (1200, 1200),而你希望它位于 (0,0),则进行变换 "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>

在您更新之前,我已经写下了大部分答案。因此,我的回答是对我认为您最初想要的内容的回应:能够直接更改 SVG 路径的 "d" 属性,以便该路径现在只填充 SVG 视口。因此,我的回答确实涉及缩放,您建议您在原始答案中确实需要但在更新中不需要。无论如何,我希望我的代码能让您了解如何在不使用转换的情况下直接更改 d 属性。

下面的代码片段以红色显示您提供的原始路径,"transformed" 路径以蓝色显示。请注意 svg 代码,前提是两条路径的起点相同。您可以获得蓝色路径的 d 属性,至少在 Firefox 中,通过右键单击路径并选择 "inspect element".

希望代码中的变量名和注释能为您提供理解我的方法所需的指导。

(更新:修复了代码片段中的代码,现在它也可以在 Chrome 和 Safari 中工作,而不仅仅是在 Firefox 中。 似乎一些 ES6 语言"let"、"const"、解构、符号等功能在 Firefox 中有效,但至少其中一些在 Chrome 或 Safari 中无效。我还没有检查 Internet Explorer 或 Opera或任何其他浏览器。)

// 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, "  ")  // sandwich each   letter between 2 spaces
    .replace(/  /g        , " "   )  // collapse repeated spaces to a single space
    .replace(/ ([Ee]) /g  , ""  )  // 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>