以数学方式转换 SVG 路径中的值以填充视图框

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

本文关键字:填充 视图 路径 方式 转换 SVG      更新时间:2023-09-26

假设我有一个看起来像这样的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 的 SVG 开始,如下所示:0 0 1600 1600 .

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

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

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

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

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

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

我不想扩展或延伸路径。

我同样对数学过程或函数或某种在线工具感到高兴。

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

(即,我不希望我的网页包含"不正确"的数据和"更正"它的转换;我只希望我的代码包含"正确"的数据。

有没有办法做到这一点?

在您

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

下面的代码片段以红色显示您提供的原始路径,"转换后"路径以蓝色显示。请注意,在提供的 svg 代码中,两条路径的开头相同。至少在 Firefox 中,您可以通过右键单击路径并选择"inspect element"来获取蓝色路径的 d 属性。

希望代码中的变量名称和注释提供了理解我的方法所需的指南。

(更新:修复了代码片段中的代码,因此它现在也可以在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, " $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>

如果您不需要缩放新路径,那么您需要做的就是应用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>