2

I am implementing a set of custom elements that will be used like this:

<om-root>
  ...
  <om-node id="node1">
    ...
    <om-node id="node2">
      ...
    </om-node>
    ...
  </om-node>
  ...
<om-root>

That is, my <om-node> elements will be mixed in with arbitrary HTML, which may have positioning and/or CSS transform applied.

The purpose of these <om-node> elements is to apply CSS affine transformations to their content based on various conditions. But regardless of its position in the hierarchy, each om-node computes a transformation relative to the root node.

I can't just apply the computed transformation to the node, because the browser will combine that with the transformations of all its ancestor elements: if I rotate node1 by 30 degrees, then node2 will also be rotated by 30 degrees before its own transformation is applied.

Ideally, what I want is something that works like Element.getClientRects(), but returns a matrix rather than just a bounding box. Then I could do some math to compensate for the difference between the coordinate systems of the <om-node> and <om-root> elements.

This question is similar to mine, but doesn't have a useful answer. The question mentions using getComputedStyle(), but that doesn't do what is claimed – getComputedStyle(elt).transform returns a transformation relative to the element's containing block, not the viewport. Plus, the result doesn't include the effects of "traditional" CSS positioning (in fact it doesn't have a value at all for traditionally-positioned elements).

So: Is there a robust way to get the transformation matrix for an element relative to the viewport?

The layout engine obviously has this info, and I'd prefer not to do a complicated (and expensive) tree-walking process every time anything changes.

bobtato
  • 1,157
  • 1
  • 9
  • 11
  • If you had access to chrome devtools API (e.g using some electron app, chrome debugger etc), there's an API which could help with this `DOM.getContentQuads` https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads – Utkarsh Dixit Aug 24 '21 at 15:21
  • I need to make this work on the web in general, so that API isn't directly useful. But that has made me think: there may be a way to work out the transformed quadrilateral of an element (and from that, an affine transformation matrix) using only getClientRects()... – bobtato Aug 25 '21 at 01:46

2 Answers2

2

Having thought some more about the question, it occurred to me that, in fact, you can solve the problem using getBoundingClientRect().

Of course, getBoundingClientRect() on its own does not tell you how an element has been transformed, because the same bounding box describes many possible transformations:

different transformations, same bounding box

However, if we add three child elements, with a known size and position relative to the parent, then we can figure out more information by comparing each of their bounding boxes. The figure below shows where I have placed these three "gauge" elements (which in practice are empty and invisible):

gauge elements

The vectors and are orthogonal unit vectors of the parent element's untransformed coordinate system. After the element has been transformed by various CSS positioning and transform properties, we first need to find the transformed unit vectors u̅' and v̅'. We can do that by comparing the bounding boxes of the three gauge elements – the diagram below shows the process with two different example transformations:

unit vectors from bounding boxes

  • the vector from box 1 to box 2 is equivalent to u̅'
  • the vector from box 1 to box 3 is equivalent to v̅'
  • the midpoint between [the top left of box 3] and [the bottom right of box 2] gives us point P: this is the transformed position of the parent element's origin

From these three values u̅', v̅' and P we can directly construct a 2D affine transformation matrix T:

derived transformation matrix

This matrix T represents all the transformations affecting the parent element – not just CSS transform rules, but also "traditional" positioning, the effects of margins and borders, etc. And, because it's calculated from getBoundingClientRect(), it is always relative to the viewport – you can compare any two elements directly, regardless of their relationship within the DOM hierarchy.

Note: all this assumes we are only dealing with 2D affine transformations, such as transform:rotate(30deg) or left:120px. Dealing with 3D CSS transforms would be more complicated, and is left as an exercise for the reader.

Putting the above into code form:

    class WonderDiv extends HTMLElement {
        constructor () {
            super();
            this.gauges = [null, null, null];
        }
        connectedCallback () {
            this.style.display = "block";
            this.style.position = "absolute";
        }
        createGaugeElement (i) {
            let g = document.createElement("div");
            // applying the critical properties via a style
            // attribute makes them harder to override by accident
            g.style = "display:block; position:absolute;"
                + "margin:0px; width:100px; height:100px;"
                + "left:" + ( ((i+1)%2) ? "-100px;" : "0px;")
                + "top:" + ( (i<2) ? "-100px;" : "0px;");
            this.appendChild(g);
            this.gauges[i] = g;
            return g;
        }
        getClientTransform () {
            let r = [];
            let i;
            for (i=0; i<3; i++) {
              // this try/catch block creates the gauge elements
              // dynamically if they are missing, so (1) they aren't
              // created where they aren't needed, and (2) they are
              // restored automatically if someone else removes them.
              try { r[i] = this.gauges[i].getBoundingClientRect(); }
              catch { r[i] = this.createGaugeElement(i).getBoundingClientRect(); }
            }
            // note the factor of 100 here - we've used 100px divs
            // instead of 1px divs, on a hunch that might be safer
            return DOMMatrixReadOnly.fromFloat64Array(new Float64Array([
                (r[1].left - r[0].left) / 100,
                (r[1].top - r[0].top) / 100,
                (r[2].left - r[0].left) / 100,
                (r[2].top - r[0].top) / 100,
                (r[1].right + r[2].left) /2,
                (r[1].top + r[2].bottom) /2
            ]));
        }
    }
    
    customElements.define("wonder-div", WonderDiv);

– the custom <wonder-div> element extends <div> to have a getClientTransform() method, which works like getClientBoundingRect() except that it returns a DOMMatrix instead of a DOMRect.

bobtato
  • 1,157
  • 1
  • 9
  • 11
0

CSS Transformations are actually relatively heavy operations and do come with some gotchas.. (they TRANSFORM elements) so you may not be able to avoid traversing the nodes without implementing an intelligent state system, for example, storing all your objects + transformation in your javascript class..

That said, one easy workaround for small use cases is to disable transform on all the parent elements using something like 'inline' but this is not suitable for all cases..

<div id="outside">
    <div id="inside">Absolute</div>
</div>

document.getElementById('outside').style.display = "inline";

The more robust approach is to retrieve and parse the computedStyles dynamically ...

function getTranslateXY(element) {
    const style = window.getComputedStyle(element)
    const matrix = new DOMMatrixReadOnly(style.transform)
    return {
        translateX: matrix.m41,
        translateY: matrix.m42
    }
}

Then you can dynamically set new transformations on any node by adding/subtracting from the current transformation state.

Chibueze Opata
  • 9,856
  • 7
  • 42
  • 65
  • Yeah, if I was only worried about translation that wouldn’t be too difficult to handle (though “conventional” CSS positioning and translation via CSS Transform are reported separately, so it’s a bit more complicated than your code suggests if both are in use). But my custom elements produce basically arbitrary transformations, with scale, rotate and shear components; and I’d also like them to work correctly with whatever CSS the user may apply in between. Unless there is an API for it, handling the general case seems fiendishly complicated (and probably slow). – bobtato Aug 24 '21 at 12:11
  • (I awarded the bounty anyway, since you were the only person who answered...) – bobtato Aug 25 '21 at 01:39