How to get transform matrix of a DOM element? Just like canvas context, do we have a getTransform() method on DOM?
-
See this [blog post](https://zellwk.com/blog/css-translate-values-in-javascript/). You can also use [css-translate-matrix](https://www.npmjs.com/package/css-translate-matrix-parser) or [transformation-matrix](https://www.npmjs.com/package/transformation-matrix) npm packages. – tao Jan 13 '22 at 12:39
1 Answers
getComputedStyle($el).transform
should return a 2 x 3 transformation matrix, IF there is any 2d transformation, and 4 x 4 matrix3d if there is transformation on the z axis too. See my answer here for how to manipulate that in case.
In case of nested elements, the resulting matrix is the multiplication of matrices starting from parent to child:
<parent>
<firstChild>
<grandChild>
</grandChild>
</firstChild>
</parent>
Above the resulting DOMMatrix would be:
✝ Mparent • MfirstChild • MgrandChild
The svg equivalent of above inside the transform attribute would be:
transform="transformparent transformfirstChildtransform grandChild
So the transformations are logically applied from RIGHT to LEFT (grandChild's transformation is applied to itself first) and the mathematical projection of this is multiply the matrices from LEFT to RIGHT, like shown in ✝ above.
There is a catch though, in SVG, all transformations are relative to absolute 0,0 origin (top left corner) of the viewBox, no matter where/what the element is. For that reason we need some special conditions:
1- Use transform-origin: 0px 0px;
:
In CSS some transformations, like rotate is relative to the center of the element, to make things easier for people. However this creates the problem of "rotate(Xdeg)" being relative to the element's width/height (default css rotate is around 50% 50%). A default css "rotate(40deg)" on square with 100px side does NOT result in the same matrix as a square with 300px. To NOT incorporate the variable width/height of the element, one thing we can do is to make sure the element uses
transform-origin: 0px 0px;
.
2- Use only transform
and no css positioning:
Make sure relative/absolute positioned elements do NOT incorporate values that modify the location of the element such as
left, top, bot, right ...
because these won't be captured in the element's transformation matrix, unless you manually callel.getBoundingClientRect()
and derive the matrix entries yourself
If these conditions are met, below example (also fiddle) should work OK. The goal is to calculate combined matrix of red (parent) + blue (child) squares and superimpose magenta/yellow with the blue square. Magenta's matrix is calculated by multiplying red's and blue's matrices left to right, yellow's right to left:
/*THIS EXAMPLE WORKS IF THE MECHANISM OF TRANSFORMATION MATRICES
IN CSS DOM IS CLOSE ENOUGH TO SVG,
WHICH MEANS LIKE IN SVG, ROTATE SHOULD NOT
BE AROUND AN ELEMENTS CENTER (default) BUT TO
THE TOP LEFT CORNER OF THE ELEMENT.
IN SVG, ALL TRANFORMATIONS ARE RELATIVE TO THE TOP LEFT
CORNER OF THE VIEWBOX WHICH CANNOT
BE 100% TRANSPORTED TO CSS.
THE CLOSEST I COULD FIND WAS TO
SET TRANSFORM-ORIGIN: 0px 0px WHICH SEEMS TO WORK */
/*There is red parent square and blue child square
inside which both have cascading transformations.
The goal is to combine their transformation matrices
and make yellow (W) and magenta (U) squares
superimpose with the blue square*/
//get all the elements in variables
const [elX, elY, elU, elW] = [...document.getElementsByTagName("div")];
//make a regexp to extract the entries of the matrix, with match, it returns [a, b, c, d, e, f]
const rgx = /(-?[0-9]+(?:\.[0-9]+)?)/gi;
//DOMMatrices expect an array of 6 numbers for 2D transformations
//get the matrix of big red parent square
const matrix_elX = new DOMMatrix(getComputedStyle(elX).transform.match(rgx));
//get the matrix of the small blue child square
const matrix_elY = new DOMMatrix(getComputedStyle(elY).transform.match(rgx));
/*
'result' is MX * MY and 'result2' is MY * MX
magenta square U uses result,
yellow square W uses result2,
magenta square U correctly superimposes with blue square,
but yellow square W does NOT,
therefore MX * MY and MY * MX are not the same, as you can expect that matrice multiplication is NOT commutative
Below uses DOMMatrix.fromMatrix is create clones of the matrices because operations does mutate the matrix itself.
*/
let result = DOMMatrix.fromMatrix(matrix_elY).preMultiplySelf(DOMMatrix.fromMatrix(matrix_elX)),
result2 = DOMMatrix.fromMatrix(matrix_elX).preMultiplySelf(DOMMatrix.fromMatrix(matrix_elY));
elU.style.transform = result;
elW.style.transform = result2;
* {
margin: 0px;
box-sizing: border-box;
transform-origin: 0px 0px;
}
#x {
position: relative;
background: red;
width: 300px;
height: 300px;
transform: translate(-20px, -20px) rotate(-30deg) translate(150px, 100px) rotate(20deg);
}
#y {
position: relative;
background: blue;
width: 100px;
height: 100px;
transform: rotate(-5deg) rotate(10deg) translate(200px, 50px) rotate(75deg) translate(35px, 10px) rotate(-25deg);
}
#u {
position: absolute;
top: 0px;
width: 100px;
height: 100px;
background: magenta;
opacity: 0.5;
}
#w {
position: absolute;
top: 0px;
width: 100px;
height: 100px;
background: yellow;
opacity: 0.5;
}
<div id="x">
<div id="y">
</div>
</div>
<div id="u">U</div>
<div id="w">W</div>

- 5,643
- 3
- 16
- 22
-
probably there is, like in SVG's, this is one "less clean" solution that worked for me in the past. – ibrahim tanyalcin Jan 13 '22 at 12:42
-
-
They are probably parsing "matrix(" or "matrix3d", so it is either parsing hell or technical debt :) – ibrahim tanyalcin Jan 13 '22 at 12:45
-
Has the advantage of having been *"more"* tested in browser than whatever we write. One less worry on the list. – tao Jan 13 '22 at 12:46
-
If E1 is a DOM element and E2 is its child. The matrics of E1 and E2 are M1 and M2 respectively. So the final transform matrix of E2 is M1 x M2? – Jiale Wang Jan 13 '22 at 13:14
-
@JialeWang , hey I was away yesterday, here is a small example that expands on your question. A lot of people including me always trip on this time to time. – ibrahim tanyalcin Jan 14 '22 at 11:38