使用JavaScript将SVG变换的元素适配到矩形边界

7

我正在解决一个问题,即如何将实用转换后的SVG元素适配到给定的矩形边界中。

  • 目标矩形是固定的而且没有进行过变换。
  • 输入矩形可能有任意类型的变换。
  • 输入矩形可以是任意变换过的群组的子元素。
  • 变换应该仅应用于输入矩形。
  • 这个问题仅涉及JavaScript元素变换。

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

When parent groups are not transformed

在这种情况下,目标矩形与输入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 给出的答案关注于将变换应用于父元素,这也是一种选择,但我的目标是将元素转换为目标矩形的边界。

1
使用transformToElement怎么样? - Robert Longson
@RobertLongson谢谢你的建议。据我所知,getTransformToElement已经过时了。无论如何,现在有一个poly fill来替代它。但是,主要问题是应该转换什么?缩放因子矩阵? - Ievgen
1
是的,对于那些已经移除它的浏览器,有一个polyfill可用。 - Robert Longson
1
当其他方法都失败时,一个可能的解决方案是将SVG作为图像绘制在画布上,从画布获取图像数据,并获取红色(在您的情况下)像素的最大和最小值,并使用这些值作为框边界。请查看此答案:https://stackoverflow.com/questions/62779904/get-the-bounding-box-of-the-intersection-of-2-or-more-paths#63203972 - enxaneta
@enxaneta 谢谢,但这个问题明确涉及 JavaScript 转换。 - Ievgen
3个回答

2

正如您所发现的那样,这是一个棘手的问题。它甚至比您想象的更加棘手(请参见后文)。

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

由于您的要求是将“输入”转换为“目标”,因此理解该问题的方法是将您的坐标空间切换到“输入”矩形的视角。从“输入”的角度来看,“目标”是什么样子的?为了看到这一点,我们需要使用“输入”具有的变换的逆变换来转换“目标”。

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

因此,你现在可以看到为什么它如此棘手。我们要么必须找到将平行四边形映射到另一个平行四边形的变换,要么是矩形映射到平行四边形。显然,我们会选择后者。你会期望这是两个选项中更简单的一个。
我们还得到了帮助,因为我们可以假设变换是仿射的。直线保持直线,平行线保持平行。
因此,我们的任务是将矩形放大,使其整齐地适合目标平行四边形内部。另外,由于平行四边形具有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>

我们离目标很近,但有点超出规格了。发生了什么?
如果我们回到之前“输入”矩形空间的视角来看,就能更清楚地看到问题所在。

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

翻译如下:
绿点代表我们从“输入”矩形水平和垂直地发射射线得到的交点。淡红色矩形表示将“输入”矩形放大以接触截距点的结果,它超出了我们的“目标”形状。这就是为什么我们之前代码段中的形状也会溢出的原因。
这就是我在最开始时所说的比你想象的要棘手的意思。要使“输入”匹配“目标”,您必须调整两个相互依存的X和Y比例尺。如果您调整X比例尺以适合,则不再适合Y方向。反之亦然。
引用:
我只想做到这一步。我已经花了几个小时来回答这个问题。也许有一个数学解决方案可以找到一个适合四边形且接触所有四条边的矩形。但我真的不想花时间去计算。抱歉。 :)
也许您或其他人可以进一步研究。您还可以尝试迭代解决方案,通过迭代地微调X和Y比例尺,直到足够接近为止。
最后,如果您愿意接受这样的条件,即在水平和垂直方向上拉伸输入,并且如果您只是将输入进行缩放以适应(即保持纵横比不变),那么这就是一个更简单的问题可以解决。

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>


非常感谢您的贡献和时间,“如果您调整X轴比例尺以适应,它将不再适合Y方向。” 这正是我面临的问题。到目前为止,我的调查告诉我,在您可以单独计算每个点并相应地缩放它们的情况下,更容易适应路径图形,因此可能将所有内容转换为路径是我要尝试和考虑的另一件事情。 - Ievgen
我刚刚检查了一下路径数据的想法是否可行:
  1. 将所有路径点转换为屏幕坐标。
  2. 缩放因子已知,是目标屏幕边界框和输入屏幕边界框之间的差异(例如:客户端边界矩形或将未变换的边界框转换为屏幕)。
  3. 使用缩放矩阵转换所有转换后的屏幕路径段点: const screen = this.node.getScreenCTM(); matrix = screen.inverse().multiply(scaleMatrix.multiply(screen));
当然,还有其他复杂性,如不同的图形和弧线等,但这也是一个选项。
- Ievgen
再次感谢您的支持。我已经通过将矩形转换为屏幕坐标来推导出这个想法,最终也成功了。您详细的解释帮助我意识到了这一点。 - Ievgen
我很高兴能够帮忙。谢谢你的额外声望奖励! - Paul LeBeau

1

更新:

我成功地将源元素调整到与目标元素匹配。我实现这一点的方法是通过相对于目标元素翻译源元素的最顶层容器,并根据源和目标元素之间的大小比率进行缩放

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>

原始回答: 我认为这不是你要找的确切答案,但更容易的做法是:

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

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>

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

1
谢谢您的回答,这个回答引发了一些想法。从第一眼看起来,多次变换有点奇怪,我相信只应用一个变换就可以完成这个任务。并且目标是适配矩形。所以两个矩形的getBoundingClientRect最终应该匹配。 - Ievgen
1
问题在于父节点仍然是一个矩形,虽然是倾斜的,因此为了将倾斜的矩形适配到正常的矩形中,如果没有反向变换,很可能会得到类似第一种方法的结果。 - Dipen Shah
1
@IevgenNaida 你需要记住 getBoundingClientRect 返回的是页面视口的边界矩形而不一定是 svg 元素的视口。 - Dipen Shah
是的,我知道。bbox坐标也可以使用getScreenCTM进行转换,以获取SVG视口。但是对于这个例子,SVG和页面应该匹配。 - Ievgen
1
我很感谢你的贡献。到目前为止看起来非常不错,我稍后将能够进行分析,但看起来就是我需要的东西。 - Ievgen
显示剩余2条评论

1

我花了一些时间才意识到答案,但最终我得到了它,而且很简单!

  1. 获取两个矩形在“屏幕”坐标系下的边界框。例如:使用 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链接


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接