使用 JavaScript 将 SVG 转换后的元素放入矩形边界

Fit SVG transformed element into the rect bounds with JavaScript

我正在努力解决将实用转换后的 SVG 元素放入给定矩形边界的问题。

当元素本身只有变换时,这是一项简单的任务:

在这种情况下,目标和输入之间的比例 getBoundingClientRect(屏幕坐标中的边界矩形)等于适当的比例因子。

但是当父元素也被转换时它不起作用:

   var inputElement = document.getElementById("input");
var destinationElement = document.getElementById("destination");


var inputBB = inputElement.getBoundingClientRect();
var outputBB = destinationElement.getBoundingClientRect();
var scaleX = outputBB.width / inputBB.width;
var scaleY = outputBB.height / inputBB.height;
// get offsets between figure center and destination rect center:
var offsetX = outputBB.x + outputBB.width / 2 - (inputBB.x + inputBB.width / 2);
var offsetY =
  outputBB.y + outputBB.height / 2 - (inputBB.y + inputBB.height / 2);

// get current figure transformation
let currentMatrix = (
  inputElement.transform.baseVal.consolidate() ||
  inputElement.ownerSVGElement.createSVGTransform()
).matrix;

// Get center of figure in element coordinates:
const inputBBox = inputElement.getBBox();
const centerTransform = inputElement.ownerSVGElement.createSVGPoint();
centerTransform.x = inputBBox.x + inputBBox.width / 2;
centerTransform.y = inputBBox.y + inputBBox.height / 2;
// create scale matrix:
const svgTransform = inputElement.ownerSVGElement.createSVGTransform();
svgTransform.setScale(scaleX, scaleY);

let scalingMatrix = inputElement.ownerSVGElement
  .createSVGMatrix()
  // move the figure to the center of the destination rect.
  .translate(offsetX, offsetY)
  // Apply current matrix, so old transformations are not lost
  .multiply(currentMatrix)
  .translate(centerTransform.x, centerTransform.y)
  // multiply is used instead of the scale method while for some reasons matrix scale is giving proportional scaling...
  // From a transforms proper matrix is generated.
  .multiply(svgTransform.matrix)
  .translate(-centerTransform.x, -centerTransform.y);

// Apply new created matrix to element back:
const newTransform = inputElement.ownerSVGElement.createSVGTransform();
newTransform.setMatrix(scalingMatrix);
inputElement.transform.baseVal.initialize(newTransform);

var bboundsTest= document.getElementById("bboundsTest");
const resultBBounds = inputElement.getBoundingClientRect();
bboundsTest.setAttribute('x', resultBBounds .x);
bboundsTest.setAttribute('y', resultBBounds .y);
bboundsTest.setAttribute('width', resultBBounds .width);
bboundsTest.setAttribute('height', resultBBounds .height);
document.getElementById('test2').innerHTML = 'expected: 100x100 . Results: ' + resultBBounds.width + 'x' + resultBBounds.height
<svg
  version="1.2"
  viewBox="0 0 480 150"
  width="480"
  height="150"
  xmlns="http://www.w3.org/2000/svg"
>

<g transform="skewX(10) translate(95,1) rotate(30)">
  <g transform="skewX(30) translate(-3,3) rotate(30)">
    <g transform="skewX(10) translate(-3,4) rotate(10)">
      <rect
        id="input"
        transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
        width="30"
        height="30"
        fill="red"
      />
    </g>
  </g>
</g>

<rect
  id="destination"
  x="20"
  y="20"
  width="100"
  height="100"
  fill="transparent"
  stroke="blue"
/>
 <rect
  id="bboundsTest"
  x="20"
  y="20"
  width="100"
  height="100"
  fill="transparent"
  stroke="black"
/>

</svg>
<div id="test2"></div>

关于如何将父转换纳入计数以找到合适的比例因子,有什么想法吗?

提前感谢您的想法!

Dipen Shah 给出的答案侧重于将转换应用于父元素,这也是一个选项,但我的目标是将元素转换为目标矩形边界。

更新:

我能够使源元素与目标元素匹配。我能够实现这一目标的方法是 相对于目标元素 翻译源元素的最顶层容器和 缩放 容器基于 源和目标之间的大小比个元素。

function applyTransformations(source, sourceContainer, target, includeMagicScaleMargin) {
  var sourceBB = source.getBoundingClientRect();
  var inputBB = sourceContainer.getBoundingClientRect();
  var outputBB = target.getBoundingClientRect();
  var scaleX = (outputBB.width - (includeMagicScaleMargin ? 10 : 0)) / sourceBB.width;
  var scaleY = (outputBB.height - (includeMagicScaleMargin ? 10 : 0)) / sourceBB.height;

  // get offsets between figure center and destination rect center:
  var offsetX = outputBB.x + outputBB.width / 2 - (inputBB.x + inputBB.width / 2);
  var offsetY =
    outputBB.y + outputBB.height / 2 - (inputBB.y + inputBB.height / 2);

  // get current figure transformation
  let currentMatrix = (
    sourceContainer.transform.baseVal.consolidate() ||
    sourceContainer.ownerSVGElement.createSVGTransform()
  ).matrix;

  // Get center of figure in element coordinates:
  const inputBBox = sourceContainer.getBBox();
  const centerTransform = sourceContainer.ownerSVGElement.createSVGPoint();
  centerTransform.x = inputBBox.x + inputBBox.width / 2;
  centerTransform.y = inputBBox.y + inputBBox.height / 2;
  // create scale matrix:
  const svgTransform = sourceContainer.ownerSVGElement.createSVGTransform();
  svgTransform.setScale(scaleX, scaleY);

  let scalingMatrix = sourceContainer.ownerSVGElement
    .createSVGMatrix()
    // move the figure to the center of the destination rect.
    .translate(offsetX, offsetY)
    // Apply current matrix, so old transformations are not lost
    .multiply(currentMatrix)
    .translate(centerTransform.x, centerTransform.y)
    // multiply is used instead of the scale method while for some reasons matrix scale is giving proportional scaling...
    // From a transforms proper matrix is generated.
    .multiply(svgTransform.matrix)
    .translate(-centerTransform.x, -centerTransform.y);

  // Apply new created matrix to element back:
  const newTransform = sourceContainer.ownerSVGElement.createSVGTransform();
  newTransform.setMatrix(scalingMatrix);

  sourceContainer.transform.baseVal.initialize(newTransform);
}

function isDescendant(parent, child) {
  var node = child.parentNode;
  while (node != null) {
    if (node == parent) {
      return true;
    }
    node = node.parentNode;
  }
  return false;
}

function transformSVG1() {
  var destinationElem = document.getElementById("destination");
  var inputElem = document.getElementById("input");
  var inputContainerElem = inputElem;

  while (inputContainerElem.parentNode != null) {
    let candidateParent = inputContainerElem.parentNode;
    if (isDescendant(candidateParent, destinationElem)) {
      break;
    }
    inputContainerElem = candidateParent;
  }

  applyTransformations(inputElem, inputContainerElem, destinationElem);
}

function transformSVG2() {
  var destinationElem = document.getElementById("destination2");
  var inputElem = document.getElementById("input2");
  var inputContainerElem = inputElem;

  while (inputContainerElem.parentNode != null) {
    let candidateParent = inputContainerElem.parentNode;
    if (isDescendant(candidateParent, destinationElem)) {
      break;
    }
    inputContainerElem = candidateParent;
  }

  applyTransformations(inputElem, inputContainerElem, destinationElem, true);
}

transformSVG1();
transformSVG2();
<svg version="1.2" viewBox="0 0 480 200" width="480" height="200" xmlns="http://www.w3.org/2000/svg">
  <g>
    <text x="0" y="20" font-size="20">No magic margins</text>
    <g transform="skewX(10) translate(95,1) rotate(30)">
      <g transform="skewX(30) translate(-3,3) rotate(30)">
        <g transform="skewX(10) translate(-3,4) rotate(10)">
          <rect id="input" transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)" width="30" height="30" fill="red" />
        </g>
      </g>
    </g>

    <rect id="destination" x="40" y="40" width="100" height="100" fill="transparent" stroke="blue" />
  </g>
</svg>

<svg version="1.2" viewBox="0 0 480 200" width="480" height="200" xmlns="http://www.w3.org/2000/svg">
  <g>
    <text x="0" y="20" font-size="20">Magic margins!</text>
    <g transform="skewX(10) translate(95,1) rotate(30)">
      <g transform="skewX(30) translate(-3,3) rotate(30)">
        <g transform="skewX(10) translate(-3,4) rotate(10)">
          <rect id="input2" transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)" width="30" height="30" fill="red" />
        </g>
      </g>
    </g>

    <rect id="destination2" x="40" y="40" width="100" height="100" fill="transparent" stroke="blue" />
  </g>
</svg>

原回答: 我认为这不是您正在寻找的确切答案,但更容易做的事情是:

方法一: 继续应用与输入元素及其父元素相同的转换,直到找到公共父节点。

function applyTransformations(source, target) {
  var inputBB = source.getBoundingClientRect();
  var outputBB = target.getBoundingClientRect();
  var scaleX = outputBB.width / inputBB.width;
  var scaleY = outputBB.height / inputBB.height;
 
  // get offsets between figure center and destination rect center:
  var offsetX = outputBB.x + outputBB.width / 2 - (inputBB.x + inputBB.width / 2);
  var offsetY =
    outputBB.y + outputBB.height / 2 - (inputBB.y + inputBB.height / 2);

  // get current figure transformation
  let currentMatrix = (
    source.transform.baseVal.consolidate() ||
    source.ownerSVGElement.createSVGTransform()
  ).matrix;

  // Get center of figure in element coordinates:
  const inputBBox = source.getBBox();
  const centerTransform = source.ownerSVGElement.createSVGPoint();
  centerTransform.x = inputBBox.x + inputBBox.width / 2;
  centerTransform.y = inputBBox.y + inputBBox.height / 2;
  // create scale matrix:
  const svgTransform = source.ownerSVGElement.createSVGTransform();
  svgTransform.setScale(scaleX, scaleY);

  let scalingMatrix = source.ownerSVGElement
    .createSVGMatrix()
    // move the figure to the center of the destination rect.
    .translate(offsetX, offsetY)
    // Apply current matrix, so old transformations are not lost
    .multiply(currentMatrix)
    .translate(centerTransform.x, centerTransform.y)
    // multiply is used instead of the scale method while for some reasons matrix scale is giving proportional scaling...
    // From a transforms proper matrix is generated.
    .multiply(svgTransform.matrix)
    .translate(-centerTransform.x, -centerTransform.y);

  // Apply new created matrix to element back:
  const newTransform = source.ownerSVGElement.createSVGTransform();
  newTransform.setMatrix(scalingMatrix);
  
  source.transform.baseVal.initialize(newTransform);
}

function isDescendant(parent, child) {
  var node = child.parentNode;
  while (node != null) {
    if (node == parent) {
      return true;
    }
    node = node.parentNode;
  }
  return false;
}

var destinationElement = document.getElementById("destination");
var inputElement = document.getElementById("input");
while (inputElement.parentNode != null) {
  applyTransformations(inputElement, destinationElement);

  let candidateParent = inputElement.parentNode;
  if (isDescendant(candidateParent, destinationElement)) {
    break;
  }
  inputElement = candidateParent;
}

// Test:
var bboundsTest= document.getElementById("bboundsTest");
const resultBBounds = document.getElementById("input").getBoundingClientRect();
bboundsTest.setAttribute('x', resultBBounds.x);
bboundsTest.setAttribute('y', resultBBounds.y);
bboundsTest.setAttribute('width', resultBBounds.width);
bboundsTest.setAttribute('height', resultBBounds.height);
<svg version="1.2" viewBox="0 0 480 240" width="480" height="240" xmlns="http://www.w3.org/2000/svg">
  <g>
    <g transform="skewX(10) translate(95,1) rotate(30)">
      <g transform="skewX(30) translate(-3,3) rotate(30)">
        <g transform="skewX(10) translate(-3,4) rotate(10)">
          <rect
            id="input"
            transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
            width="30"
            height="30"
            fill="red"
          />
        </g>
      </g>
    </g>

    <rect
      id="destination"
      x="20"
      y="20"
      width="100"
      height="100"
      fill="transparent"
      stroke="blue"
    />

 <rect
  id="bboundsTest"
  fill="transparent"
  stroke="black"
/>
  </g>
</svg>

方法二: 或者首先找到不是目标父节点的输入父节点,然后应用与父节点相同的转换。

function applyTransformations(source, target) {
  var inputBB = source.getBoundingClientRect();
  var outputBB = target.getBoundingClientRect();
  var scaleX = outputBB.width / inputBB.width;
  var scaleY = outputBB.height / inputBB.height;
 
  // get offsets between figure center and destination rect center:
  var offsetX = outputBB.x + outputBB.width / 2 - (inputBB.x + inputBB.width / 2);
  var offsetY =
    outputBB.y + outputBB.height / 2 - (inputBB.y + inputBB.height / 2);

  // get current figure transformation
  let currentMatrix = (
    source.transform.baseVal.consolidate() ||
    source.ownerSVGElement.createSVGTransform()
  ).matrix;

  // Get center of figure in element coordinates:
  const inputBBox = source.getBBox();
  const centerTransform = source.ownerSVGElement.createSVGPoint();
  centerTransform.x = inputBBox.x + inputBBox.width / 2;
  centerTransform.y = inputBBox.y + inputBBox.height / 2;
  // create scale matrix:
  const svgTransform = source.ownerSVGElement.createSVGTransform();
  svgTransform.setScale(scaleX, scaleY);

  let scalingMatrix = source.ownerSVGElement
    .createSVGMatrix()
    // move the figure to the center of the destination rect.
    .translate(offsetX, offsetY)
    // Apply current matrix, so old transformations are not lost
    .multiply(currentMatrix)
    .translate(centerTransform.x, centerTransform.y)
    // multiply is used instead of the scale method while for some reasons matrix scale is giving proportional scaling...
    // From a transforms proper matrix is generated.
    .multiply(svgTransform.matrix)
    .translate(-centerTransform.x, -centerTransform.y);

  // Apply new created matrix to element back:
  const newTransform = source.ownerSVGElement.createSVGTransform();
  newTransform.setMatrix(scalingMatrix);
  
  source.transform.baseVal.initialize(newTransform);
}

function isDescendant(parent, child) {
  var node = child.parentNode;
  while (node != null) {
    if (node == parent) {
      return true;
    }
    node = node.parentNode;
  }
  return false;
}

var destinationElement = document.getElementById("destination");
var inputElement = document.getElementById("input");
while (inputElement.parentNode != null) {
  let candidateParent = inputElement.parentNode;
  if (isDescendant(candidateParent, destinationElement)) {
    break;
  }
  inputElement = candidateParent;
}

applyTransformations(inputElement, destinationElement);
// Test:
var bboundsTest= document.getElementById("bboundsTest");
const resultBBounds = document.getElementById("input").getBoundingClientRect();
bboundsTest.setAttribute('x', resultBBounds.x);
bboundsTest.setAttribute('y', resultBBounds.y);
bboundsTest.setAttribute('width', resultBBounds.width);
bboundsTest.setAttribute('height', resultBBounds.height);
<svg version="1.2" viewBox="0 0 480 240" width="480" height="240" xmlns="http://www.w3.org/2000/svg">
  <g>
    <g transform="skewX(10) translate(95,1) rotate(30)">
      <g transform="skewX(30) translate(-3,3) rotate(30)">
        <g transform="skewX(10) translate(-3,4) rotate(10)">
          <rect
            id="input"
            transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
            width="30"
            height="30"
            fill="red"
          />
        </g>
      </g>
    </g>

    <rect
      id="destination"
      x="20"
      y="20"
      width="100"
      height="100"
      fill="transparent"
      stroke="blue"
    />
 <rect
  id="bboundsTest"
  fill="transparent"
  stroke="black"
/>
  </g>
</svg>

注意:这两种方法都会根据父元素上涉及的转换产生不同的结果,因为第二种方法不会将所有转换应用于目标,而是将相同的转换应用于输入的父节点那也不是目标的父项。

正如您所发现的,这是一个棘手的问题。它甚至比你想象的还要棘手(见下文)。

您在两个不同的坐标 space 中有矩形。其中一个被改造了。因此,您正试图将一个变换后的矩形映射到另一个可能变换后的矩形。由于它们被转换,这些矩形中的一个或两个(可能)不再是矩形。

由于您的要求是将“输入”转换为“目的地”,因此解决问题的方法是将您的坐标 space 切换到“输入”的视角矩形。从“输入”的角度来看,“目的地”是什么样的?要看到,我们需要用“输入”具有的变换的逆变换来变换“目标”。

<rect id="input" transform=""/>

的目的地是什么样的

<svg
  version="1.2"
  viewBox="-50 -50 160 260"
  height="500"
  xmlns="http://www.w3.org/2000/svg"
>

<rect
  id="input"
  transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
  width="30"
  height="30"
  fill="red"
/>

<g transform="rotate(-10) translate(3,-4) skewX(-10)">
<g transform="rotate(-30) translate(3,-3) skewX(-30)">
<g transform="rotate(-30) translate(-95,-1) skewX(-10)">
<rect
  id="destination"
  x="20"
  y="20"
  width="100"
  height="100"
  fill="transparent"
  stroke="blue"
/>
</g>
</g>
</g>

<rect id="input"/>

的目的地是什么样的

<svg
  version="1.2"
  viewBox="-80 -70 120 230"
  height="500"
  xmlns="http://www.w3.org/2000/svg"
>

<rect
  id="input"
  width="30"
  height="30"
  fill="red"
/>

<g transform="rotate(-45) translate(0,0) translate(50,50) scale(0.67) translate(-50,-50) skewX(-25) translate(-95,-76.5)">
<g transform="rotate(-10) translate(3,-4) skewX(-10)">
<g transform="rotate(-30) translate(3,-3) skewX(-30)">
<g transform="rotate(-30) translate(-95,-1) skewX(-10)">
<rect
  id="destination"
  x="20"
  y="20"
  width="100"
  height="100"
  fill="transparent"
  stroke="blue"
/>
</g>
</g>
</g>
</g>

所以,您现在可以明白为什么它如此棘手了。我们要么必须找到将平行四边形映射到另一个平行四边形,或者将矩形映射到平行四边形的变换。显然我们要选择后者。您会期望它是两个选项中更简单的一个。

我们也得到了帮助,因为我们可以假设转换是 affine。直线保持直线,平行线保持平行。

所以我们的任务是放大我们的矩形,以便它整齐地适合我们的目标平行四边形。另外,由于平行四边形具有 180° 旋转对称性,因此我们知道拟合矩形的中心将与平行四边形的中心重合。

所以,让我们想象“输入”矩形位于“目标”平行四边形的中心,然后从矩形中射出假想的光线,直到它们到达平行四边形的边。无论哪条光线首先击中目标平行四边形,都会为我们提供应应用于矩形以使其适合的比例。

.ray {
  stroke: lightgrey;
  stroke-dasharray: 2 2;
}
<svg
  version="1.2"
  viewBox="0 0 120 230"
  height="500"
  xmlns="http://www.w3.org/2000/svg"
>

<g transform="translate(47.1,101.2)"><!-- positioning conveniently for our figure -->
  <!-- scaling rays -->
  <line class="ray" x1="-100" y1="0" x2="100" y2="0"/>
  <line class="ray" x1="-100" y1="30" x2="100" y2="30"/>
  <line class="ray" x1="0" y1="-100" x2="0" y2="100"/>
  <line class="ray" x1="30" y1="-100" x2="30" y2="100"/>

  <rect
    id="input"
    width="30"
    height="30"
    fill="red"
  />
  
</g>

<g transform="translate(80,70)"><!-- positioning conveniently for our figure -->

  <g transform="rotate(-45) translate(0,0) translate(50,50) scale(0.67) translate(-50,-50) skewX(-25) translate(-95,-76.5)">
  <g transform="rotate(-10) translate(3,-4) skewX(-10)">
  <g transform="rotate(-30) translate(3,-3) skewX(-30)">
  <g transform="rotate(-30) translate(-95,-1) skewX(-10)">
  <rect
   id="destination"
   x="20"
   y="20"
   width="100"
   height="100"
   fill="transparent"
   stroke="blue"
  />
  </g>
  </g>
  </g>
  </g>
  
</g>

var inputElement = document.getElementById("input");
var destinationElement = document.getElementById("destination");
var svg = inputElement.ownerSVGElement;

// Get the four corner points of rect "input"
var inX = inputElement.x.baseVal.value;
var inY = inputElement.y.baseVal.value;
var inW = inputElement.width.baseVal.value;
var inH = inputElement.height.baseVal.value;

// Get the four corner points of rect "destination"
var destX = destinationElement.x.baseVal.value;
var destY = destinationElement.y.baseVal.value;
var destW = destinationElement.width.baseVal.value;
var destH = destinationElement.height.baseVal.value;
var destPoints = [
   createPoint(svg, destX,         destY),
   createPoint(svg, destX + destW, destY),
   createPoint(svg, destX + destW, destY + destH),
   createPoint(svg, destX,         destY + destH)
];

// Get total transform applied to input rect
var el = inputElement;
var totalMatrix = el.transform.baseVal.consolidate().matrix;
// Step up ancestor tree till we get to the element before the root SVG element
while (el.parentElement.ownerSVGElement != null) {
  el = el.parentElement;
  if (el.transform) {
    totalMatrix = el.transform.baseVal.consolidate().matrix.multiply( totalMatrix );
  }
}
//console.log("totalMatrix = ",totalMatrix);

// Transform the four "destination" rect corner points by the inverse of the totalMatrix
// We will then have the corner points in the same coordinate space as the "input" rect
for (var i=0; i<4; i++) {
  destPoints[i] = destPoints[i].matrixTransform(totalMatrix.inverse());
}
//console.log("transformed destPoints=",destPoints);

// Find the equation for the rays that start at the centre of the "input" rect & "destination" parallelogram
// and pass through the corner points of the "input" rect.
var destMinX = Math.min(destPoints[0].x, destPoints[1].x, destPoints[2].x, destPoints[3].x);
var destMaxX = Math.max(destPoints[0].x, destPoints[1].x, destPoints[2].x, destPoints[3].x);
var destMinY = Math.min(destPoints[0].y, destPoints[1].y, destPoints[2].y, destPoints[3].y);
var destMaxY = Math.max(destPoints[0].y, destPoints[1].y, destPoints[2].y, destPoints[3].y);
var destCentreX = (destMinX + destMaxX) / 2;
var destCentreY = (destMinY + destMaxY) / 2;

// Find the scale in the X direction by shooting rays horizontally from the top and bottom of the "input" rect
var scale1 = findDistanceToDestination(destCentreX, destCentreY - inH/2, inW/2, 0, // line equation of ray line 1
                                       destPoints);
var scale2 = findDistanceToDestination(destCentreX, destCentreY + inH/2, inW/2, 0, // line equation of ray line 2
                                       destPoints);
var scaleX = Math.min(scale1, scale2);

// Find the scale in the Y direction by shooting rays vertically from the left and right of the "input" rect
scale1 = findDistanceToDestination(destCentreX - inW/2, destCentreY, 0, inH/2, // line equation of ray line 1
                                   destPoints);
scale2 = findDistanceToDestination(destCentreX + inW/2, destCentreY, 0, inH/2, // line equation of ray line 2
                                   destPoints);
var scaleY = Math.min(scale1, scale2);


// Now we can position and scale the "input" element to fit the "destination" rect
inputElement.transform.baseVal.appendItem( makeTranslate(svg, destCentreX, destCentreY));
inputElement.transform.baseVal.appendItem( makeScale(svg, scaleX, scaleY));
inputElement.transform.baseVal.appendItem( makeTranslate(svg, -(inX + inW)/2, -(inY + inH)/2));

function createPoint(svg, x, y)
{
  var pt = svg.createSVGPoint();
  pt.x = x;
  pt.y = y;
  return pt;
}

function makeTranslate(svg, x, y)
{
  var t = svg.createSVGTransform();
  t.setTranslate(x, y);
  return t;
}

function makeScale(svg, sx, sy)
{
  var t = svg.createSVGTransform();
  t.setScale(sx, sy);
  return t;
}

function findDistanceToDestination(centreX, centreY, rayX, rayY, // line equation of ray
                                   destPoints)                           // parallelogram points
{
  // Test ray against each side of the dest parallelogram
  for (var i=0; i<4; i++) {
    var from = destPoints[i];
    var to   = destPoints[(i + 1) % 4];
    var dx =  to.x - from.x;
    var dy =  to.y - from.y;
    var k = intersection(centreX, centreY, rayX, rayY,    // line equation of ray
                         from.x, from.y, dx, dy); // line equation of parallogram side
    if (k >= 0 && k <= 1) {
       // Ray intersected with this side
       var interceptX = from.x + k * dx;
       var interceptY = from.y + k * dy;
       var distanceX = interceptX - centreX;
       var distanceY = interceptY - centreY;
       if (rayX != 0)
         return Math.abs(distanceX / rayX);
       else if (rayY != 0)
         return Math.abs(distanceY / rayY);
       else
         return 0;  // How to handle case where "input" rect has zero width or height?
    }
  }
  throw 'Should have intersected one of the sides!'; // Shouldn't happen
}

// Returns the position along the 'side' line, that the ray hits.
// If it intersects the line, thre return value will be between 0 and 1.
function intersection(rayX, rayY, rayDX, rayDY,
                      sideX, sideY, sideDX, sideDY)
{
  // We want to find where:
  //    rayXY + t * rayDXDY = sideXY + k * sideDXDY
  // Returning k.
  // See: https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
  var den = -rayDX * -sideDY - -rayDY * -sideDX;
  return (den != 0) ? - (-rayDX * (rayY-sideY) - -rayDY * (rayX-sideX)) / den
                    : -9999;  // Lines don't intersect. Return a value outside range 0..1.
}
<svg
  version="1.2"
  viewBox="0 0 480 150"
  width="480"
  height="150"
  xmlns="http://www.w3.org/2000/svg"
>

<g transform="skewX(10) translate(95,1) rotate(30)">
  <g transform="skewX(30) translate(-3,3) rotate(30)">
    <g transform="skewX(10) translate(-3,4) rotate(10)">
      <rect
        id="input"
        transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
        width="30"
        height="30"
        fill="red"
      />
    </g>
  </g>
</g>

<rect
  id="destination"
  x="20"
  y="20"
  width="100"
  height="100"
  fill="transparent"
  stroke="blue"
/>

</svg>
<div id="test2"></div>

我们接近了,但我们有点过大了。发生什么事了?

如果我们回到“输入”矩形 space 中查看它,就像以前一样,我们可以更好地看到问题。

<svg width="500" height="500" viewBox="-40 -40 50 180">

  <polygon points="-38.5008,  79.5321,
                   -32.7704, -35.2044,
                     3.5896,  12.3685,
                    -2.1406, 127.1050"
           fill="none"
           stroke="blue"
           stroke-width="0.5"/>

  <!-- input -->
  <rect x="-32.4555" y="30.9503" width="30" height="30"
        fill="red"/>

  <!-- centre of dest -->
  <circle cx="-17.4555" cy="45.9503" r="1"/>

  <!-- intercepts X -->
  <circle cx="-36.0744" cy="30.9503" r="1" fill="green"/>
  <circle cx="-37.5727" cy="60.9503" r="1" fill="green"/>

  <!-- intercepts Y -->
  <circle cx="-32.4555" cy="-34.7923" r="1" fill="green"/>
  <circle cx="-2.4555" cy="4.4590" r="1" fill="green"/>

  <!-- scaled input -->
  <rect x="-32.4555" y="30.9503" width="30" height="30"
        fill="red" fill-opacity="0.2"
        transform="translate(-17.4556 45.9503) scale(1.24126 2.76608) translate(17.4556 -45.9503)"/>

</svg>

绿点代表我们从“输入”矩形水平和垂直发射光线得到的交点。褪色的红色矩形表示按比例放大以接触我们的截取点的“输入”矩形。它溢出了我们的“目的地”形状。这就是为什么我们之前片段中的形状也溢出了。

这就是我的意思,在最上面,当我说它比你想象的要棘手时。要使“输入”与“目标”匹配,您必须调整两个 inter-dependent X 和 Y 比例。如果您调整 X 比例尺以适合,它将不再适合 Y 方向。反之亦然。

This is as far as I want to go. I've spent a couple of hours on this answer already. Perhaps their's a mathematical solution for finding a rectangle that fits inside a parallelogram and touches all four sides. But I don't really want to spend the time to work it out. Sorry. :)

Perhaps you or someone else can take this further. You could also try an iterative solution that nudges the X and Y scales iteratively until it gets close enough.

最后,如果您准备好接受 水平和垂直拉伸输入的条件,并且您可以只按比例放大(或缩小) ) 适合的输入(即保持纵横比相同),那么这是一个更容易解决的问题。

var inputElement = document.getElementById("input");
var destinationElement = document.getElementById("destination");
var svg = inputElement.ownerSVGElement;

// Get the four corner points of rect "input"
var inX = inputElement.x.baseVal.value;
var inY = inputElement.y.baseVal.value;
var inW = inputElement.width.baseVal.value;
var inH = inputElement.height.baseVal.value;

// Get the four corner points of rect "destination"
var destX = destinationElement.x.baseVal.value;
var destY = destinationElement.y.baseVal.value;
var destW = destinationElement.width.baseVal.value;
var destH = destinationElement.height.baseVal.value;
var destPoints = [
   createPoint(svg, destX,         destY),
   createPoint(svg, destX + destW, destY),
   createPoint(svg, destX + destW, destY + destH),
   createPoint(svg, destX,         destY + destH)
];

// Get total transform applied to input rect
var el = inputElement;
var totalMatrix = el.transform.baseVal.consolidate().matrix;
// Step up ancestor tree till we get to the element before the root SVG element
while (el.parentElement.ownerSVGElement != null) {
  el = el.parentElement;
  if (el.transform) {
    totalMatrix = el.transform.baseVal.consolidate().matrix.multiply( totalMatrix );
  }
}
//console.log("totalMatrix = ",totalMatrix);

// Transform the four "destination" rect corner points by the inverse of the totalMatrix
// We will then have the corner points in the same coordinate space as the "input" rect
for (var i=0; i<4; i++) {
  destPoints[i] = destPoints[i].matrixTransform(totalMatrix.inverse());
}
//console.log("transformed destPoints=",destPoints);

// Find the equation for the rays that start at the centre of the "input" rect & "destination" parallelogram
// and pass through the corner points of the "input" rect.
var destMinX = Math.min(destPoints[0].x, destPoints[1].x, destPoints[2].x, destPoints[3].x);
var destMaxX = Math.max(destPoints[0].x, destPoints[1].x, destPoints[2].x, destPoints[3].x);
var destMinY = Math.min(destPoints[0].y, destPoints[1].y, destPoints[2].y, destPoints[3].y);
var destMaxY = Math.max(destPoints[0].y, destPoints[1].y, destPoints[2].y, destPoints[3].y);
var destCentreX = (destMinX + destMaxX) / 2;
var destCentreY = (destMinY + destMaxY) / 2;

// Shoot diagonal rays from the centre through two adjacent corners of the "input" rect.
// Whichever one hits the destination shape first, provides the scaling factor we need.
var scale1 = findDistanceToDestination(destCentreX, destCentreY, inW/2, inH/2, // line equation of ray line 1
                                       destPoints);
var scale2 = findDistanceToDestination(destCentreX, destCentreY, -inW/2, inW/2, // line equation of ray line 2
                                       destPoints);
var scale = Math.min(scale1, scale2);

// Now we can position and scale the "input" element to fit the "destination" rect
inputElement.transform.baseVal.appendItem( makeTranslate(svg, destCentreX, destCentreY));
inputElement.transform.baseVal.appendItem( makeScale(svg, scale, scale));
inputElement.transform.baseVal.appendItem( makeTranslate(svg, -(inX + inW)/2, -(inY + inH)/2));

function createPoint(svg, x, y)
{
  var pt = svg.createSVGPoint();
  pt.x = x;
  pt.y = y;
  return pt;
}

function makeTranslate(svg, x, y)
{
  var t = svg.createSVGTransform();
  t.setTranslate(x, y);
  return t;
}

function makeScale(svg, sx, sy)
{
  var t = svg.createSVGTransform();
  t.setScale(sx, sy);
  return t;
}

function findDistanceToDestination(centreX, centreY, rayX, rayY, // line equation of ray
                                   destPoints)                           // parallelogram points
{
  // Test ray against each side of the dest parallelogram
  for (var i=0; i<4; i++) {
    var from = destPoints[i];
    var to   = destPoints[(i + 1) % 4];
    var dx =  to.x - from.x;
    var dy =  to.y - from.y;
    var k = intersection(centreX, centreY, rayX, rayY,    // line equation of ray
                         from.x, from.y, dx, dy); // line equation of parallogram side
    if (k >= 0 && k <= 1) {
       // Ray intersected with this side
       var interceptX = from.x + k * dx;
       var interceptY = from.y + k * dy;
       var distanceX = interceptX - centreX;
       var distanceY = interceptY - centreY;
       if (rayX != 0)
         return Math.abs(distanceX / rayX);
       else if (rayY != 0)
         return Math.abs(distanceY / rayY);
       else
         return 0;  // How to handle case where "input" rect has zero width or height?
    }
  }
  throw 'Should have intersected one of the sides!'; // Shouldn't happen
}

// Returns the position along the 'side' line, that the ray hits.
// If it intersects the line, thre return value will be between 0 and 1.
function intersection(rayX, rayY, rayDX, rayDY,
                      sideX, sideY, sideDX, sideDY)
{
  // We want to find where:
  //    rayXY + t * rayDXDY = sideXY + k * sideDXDY
  // Returning k.
  // See: https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
  var den = -rayDX * -sideDY - -rayDY * -sideDX;
  return (den != 0) ? - (-rayDX * (rayY-sideY) - -rayDY * (rayX-sideX)) / den
                    : -9999;  // Lines don't intersect. Return a value outside range 0..1.
}
<svg
  version="1.2"
  viewBox="0 0 480 150"
  width="480"
  height="150"
  xmlns="http://www.w3.org/2000/svg"
>

<g transform="skewX(10) translate(95,1) rotate(30)">
  <g transform="skewX(30) translate(-3,3) rotate(30)">
    <g transform="skewX(10) translate(-3,4) rotate(10)">
      <rect
        id="input"
        transform="translate(95,76.5) skewX(25) translate(50,50) scale(1.5) translate(-50,-50) translate(0,0) rotate(45)"
        width="30"
        height="30"
        fill="red"
      />
    </g>
  </g>
</g>

<rect
  id="destination"
  x="20"
  y="20"
  width="100"
  height="100"
  fill="transparent"
  stroke="blue"
/>

</svg>
<div id="test2"></div>

我花了一些时间才找到答案,但最终我明白了,而且非常简单!

  1. 获取'screen'坐标中两个矩形的边界框。 例如:getBoundingClientRect.
  2. 通过比较矩形框,您可以获得所需的比例因子。
  3. 虽然应该在屏幕坐标中进行缩放,但我们应该将当前元素转换(包括所有父转换)转换为屏幕坐标,转换所有具有给定比例的转换并转换回元素坐标。

确切行是:

var toScreenMatrix = inputElement.getScreenCTM();
// Scale element by a matrix in screen coordinates and convert it back to the element coordinates:
currentMatrix = currentMatrix.multiply(toScreenMatrix.inverse().multiply(scaleAndTransform).multiply(toScreenMatrix));
    

此代码对所有 svg 元素都是通用的,因此任何形状都可以适合给定的矩形:

    function fitElement(from, to, changePosition) {
        var inputElement = document.getElementById(from);
        var destinationElement = document.getElementById(to);
        // Get center of figure in element coordinates:
        var inputScreenBBox = inputElement.getBoundingClientRect();
        var destinationScreenBBox = destinationElement.getBoundingClientRect();
        var scaleX = destinationScreenBBox.width / inputScreenBBox.width;
        var scaleY = destinationScreenBBox.height / inputScreenBBox.height;

        var inputCenter = getCenter(inputScreenBBox);
        var offsetX = 0;
        var offsetY = 0;
        if (changePosition) {
            var destCenter = getCenter(destinationScreenBBox);
            offsetX = destCenter.x - inputCenter.x;
            offsetY = destCenter.y - inputCenter.y;
        }

        // create scale matrix:
        var scaleMatrix = getScaleMatrix(scaleX, scaleY, inputElement);
        // get element self transformation matrix:
        var currentMatrix = getElementMatrix(inputElement);

        scaleAndTransform = inputElement.ownerSVGElement.createSVGMatrix()
            .translate(offsetX, offsetY)
            // Scale in screen coordinates around the element center:
            .translate(inputCenter.x, inputCenter.y)
            .multiply(scaleMatrix)
            .translate(-inputCenter.x, -inputCenter.y)
        
        var toScreenMatrix = inputElement.getScreenCTM();
        // Scale element by a matrix in screen coordinates and convert it back to the element coordinates:
        currentMatrix = currentMatrix.multiply(toScreenMatrix.inverse().multiply(scaleAndTransform).multiply(toScreenMatrix));
        // Apply new created transform back to the element:
        var newTransform = inputElement.ownerSVGElement.createSVGTransform();
        newTransform.setMatrix(currentMatrix);
        inputElement.transform.baseVal.initialize(newTransform);

    }
    function getElementMatrix(element) {
        // Get consolidated element matrix:
        var currentMatrix =
            (element.transform.baseVal.consolidate() ||
                element.ownerSVGElement.createSVGTransform()).matrix;
        return currentMatrix;
    }
    function getScaleMatrix(scaleX, scaleY, el) {
        // Return DOM matrix
        var svgTransform = el.ownerSVGElement.createSVGTransform();
        // Transform type is used because of the bug in chrome applying scale to the DOM matrix:
        svgTransform.setScale(scaleX, scaleY);
        var scaleMatrix = svgTransform.matrix;
        return scaleMatrix
    }

    function getCenter(rect) {
        return new DOMPoint((rect.x + rect.width / 2), (rect.y + rect.height / 2));
    }

    fitElement('source', 'destination', true);
<svg width="1380" height="1340" xmlns="http://www.w3.org/2000/svg">
<g transform="skewX(10) translate(-3,4) rotate(30)">
<g transform="skewX(30) translate(-3,4) rotate(30)">
<g transform="skewX(10) translate(-3,4) rotate(10)">
<g transform="translate(350,30) skewX(10) rotate(30)">
<rect id="source" transform="scale(2) rotate(30) skewX(10)" x="20" y="50" width="30" height="30"
fill="red" />
</g>
</g>
</g>
</g>
<rect id="destination" x="30" y="30" width="120" height="100" fill="transparent" stroke="blue" />
</svg>

GitHub gist link