Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fit SVG transformed element into the rect bounds with JavaScript

I am struggling with an issue to fit pragmatically transformed SVG element into the given rect bounds.

  • Destination rect is given and not transformed.
  • Input rect has any type of transformations.
  • Input rect can be a child of any transformed groups.
  • Transformations should be applied only to the input rect.
  • This question is only about the JavaScript element transformations.

It's an easy task when the element has only transformations by itself:

When parent groups are not transformed

In this case proportion between the destination and input getBoundingClientRect (bounding rect in screen coordinates) is equals to a proper scaling factor.

But it's not working when parent elements are also transformed:

   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>

Any ideas on how to take parent transformations into the count to find proper scaling factors?

Thanks in advance for the ideas!

The given answer from Dipen Shah is focused on applying transformations to the parent element and this is also an option, but my goal is transforming the element to the destination rect bounds.

like image 462
Ievgen Avatar asked Sep 14 '20 09:09

Ievgen


2 Answers

As you have discovered, this is a tricky problem. It's even trickier than you think (see later).

You have rectangles in two different corrdinate spaces. One of them is transformed. So you are trying to map one transformed rectangle to another, possibly transformed, rectangle. Since they are transformed, one or both of those rectangles is (probably) no longer a rectangle.

Since your requirement is to transform the "input" to the "destination", the way to get your head around the problem is to switch your coordinate space to the point of view of the "input" rect. What does the "destination" look like from the point of view of "input"? To see, we need to transform "destination" with the inverse of the transform that "input" has.

What the destination looks like to the <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>

What the destination looks like to the <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>

So, you can see why it's so tricky now. We either have to find the transform that maps a parallelogram to another parallelogram, or a rectangle to a parallelogram. Obviously we'll want to choose the latter. You'd expect it to be the simpler of the two options.

We are also helped because we can assume that the transformations are affine. Straight lines stay straight, and parallel lines stay parallel.

So our task is to scale up our rectangle, so that it neatly fits inside our destination parallelogram. Also, because the parallelogram has 180° rotational symmetry, we know that the centre of our fitted rectangle will coincide with the centre of the parallelogram.

So, let's imagine the "input" rectangle is sitting at the centre of the "destination" parallelogram, then shoot imaginary rays out of the rectangle until they hit the sides of the parallelogram. Whichever ray hits the destination parallelogram first, gives us the scale we should apply to the rectangle to make it fit.

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

We got close, but we're a little oversized. What happened?

If we go back to looking at it in "input" rect space, like before, we can see the problem better.

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

The green dots represent the intersection points we got from shooting the rays horizontally and vertically from our "input" rectangle. The faded red rectangle represents the "input" rectangle scaled up to touch our intercept points. It overflows our "destination" shape. Which is why our shape from the previous snippet overflows, also.

This is what I meant, at the very top, when I said it is trickier than you think. To make the "input" match the "destination", you have to tweak two inter-dependent X and Y scales. If you adjust the X scale to fit, it'll no long fit in the Y direction. And vice versa.

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.

Finally, if you are prepared to accept the condition that you don't stretch the input both horizontally and vertically, and if you are okay with just scaling up (or down) the input to fit (ie keeping the aspect ratio the same), then that's a simpler thing to solve.

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>
like image 58
Paul LeBeau Avatar answered Sep 28 '22 23:09

Paul LeBeau


Update:

I was able to fit source element to match target element. The way I was able to achieve that is by translating top most container of the source element relative to target element and scaling container based on size ratio between source and target elements.

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>

Original answer: I don't think this is an exact answer to what you are looking for but easier thing to do would be either:

Approach 1: keep on applying same transformation as input element and its parent until common parent node is found.

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>

Approach 2: Or find parent of input that is not parent of destination first and than apply same transformations as parent node.

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>

Note: Both approach will yield different results based on transformations involved on parent elements as second approach doesn't apply all transformations to destination but rather same transformations as parent node of input that is not also parent for destination.

like image 27
Dipen Shah Avatar answered Sep 28 '22 21:09

Dipen Shah