I am struggling with an issue to fit pragmatically transformed SVG element into the given rect bounds.
It's an easy task when the element has only transformations by itself:
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.
It took me some time to realize an answer, but finally, I got it and it's quite simple!
Exact line is:
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));
This code is generic for all the svg elements, so any shape can be fit into the given rect:
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>