Get DOM element exact position recursively including parent rotations and translations

269 Views Asked by At

Context

I'm trying to create a formula that allow me to get the exact position of a DOM element in the highest ancestor landmark. There are some specific rules in my probleme. Each element are absolute positionned and all element have a known top and left value in pixels. Each element can also have a rotation transform with a known value in degres. The rotation transform is also performed from the top left corner of the element.

To make it more understandable there is an exemple as an image just below.

enter image description here

In this exemple we have an arborescence composed of 3 elements. The element C is child of element B and element B is child of element A. To make it as simple as possible consider those 3 elements as simple <div> tag. Each of these elements have a known position and rotation angle in the parent landmark.

The probleme

I know the top and left value of each div element in its direct parent landmark. But i want to find the top left corner position of the element C in the general landmark representated as the landmark of the element A.

To achieve that i used homogenous transformation matrix. I calculate the general transform matrix of each element combining the translation and rotation matrix.

The rotation matrix of an element explained as :

enter image description here

With t the angle in radient of a given element in its parent landmark.

The translation matrix of an element explained as :

enter image description here

With top, left respectively the offset of the element from its parent origin top left corner.

Let's call Mr.x the rotation matrix of the element x.

Let's call Mt.x the translation matrix of the element x.

Let's call Mg.x the global transformation matrix of the element x. Obtained by doing Mr.x * Mt.x.

Let's call Cx.y the coordinates of the top left element x in the y element landmark.

The calcul

So i know Cc.b, Cb.a and Ca.g with g the general landmark. And i want Cc.g or the coordinates of the top left of element C in the general landmark.

So what i first did is accumulating all the transformation matrix of all parents elements starting with a 3 dimensions identity matrix. let's call this matrix Gtm for general transformation matrix.

The calcul is :

Gtm = Gtm * Mg.a

Gtm = Gtm * Mg.b

Then i create a "homogenous" point with the offset of the element C in the element B like below:

p = (6, 1.5, 0)

Then i multiply my point with the general transform matrix Gtm to get this point in the general landmark.

point = p * Gtm.

But that given point isn't correct and isn't integrating all the offsets of the parent correctly i don't get the correct coordinates and i don't know what i'm missing on here.

Sorry my Math and representations are approximatives. Ask me anything to get more informations if i'm not clear enough. And thank for any time spent on this.

Edit 1

Consider a Js object which a reference to a dom element and a reference to it's parent object.

let elA = {
  DOM_element = /* The <div> A */
  parent = null;
}
let elB = {
  DOM_element = /* The <div> B */
  parent = elA;
}
let elC = {
  DOM_element = /* The <div> C */
  parent = elB;
}

Consider that each of these objects have three methods respectively called getTranslationMatrix, getRotationMatrix, and getTransformMatrix, and that each of these objects have an attribute named rotation with a rotation value in degree

The code :

  /**
  * Get the translation matrix of the element 
  */
  getTranslationMatrix() {
    // Create the translation matrix of the element
    let translationMatrix = matrix([
      [1, 0, this.DOM_element.offsetLeft],
      [0, 1, this.DOM_element.offsetTop],
      [0, 0, 1],
    ]);

    return translationMatrix;
  }

  /**
  * Get the rotation matrix of the element 
  */
  getRotationMatrix() {
    // Create the rotation matrix of the element
    let rotationMatrix = matrix([
      [Math.cos(this.rotation * (Math.PI/180)), Math.sin(this.rotation * (Math.PI/180)), 0],
      [-Math.sin(this.rotation * (Math.PI/180)), Math.cos(this.rotation * (Math.PI/180)), 0],
      [0, 0, 1],
    ]);

    return rotationMatrix;
  }

  /**
  * Get the transform matrix of the element 
  */
  getTransformMatrix() {
    // Multiply all transform matrixes
    return multiply(this.getRotationMatrix(), this.getTranslationMatrix());
  }

matrix and multiply are fonctions provided by the mathjs library.

Now the code executed in the object elC :

// Get the reversed context stack
    let contextStack = [];
    let context = this.parent;
    while(context) {
      contextStack.unshift(context);
      context = context.parent;
    }

    // Create the transform matrix
    let transformMatrix = matrix([
      [1, 0, 0],
      [0, 1, 0],
      [0, 0, 1]
    ]);
    contextStack.forEach(context => {
      transformMatrix = multiply(context.getTransformMatrix(), transformMatrix);
    });

    // Create the origin point
    let origin = [
      this.DOM_element.offsetLeft,
      this.DOM_element.offsetTop,
      1
    ];

    // Apply the transform matrix
    origin = multiply(origin, transformMatrix);

And i should get the correct coordinates in the origin point but that is not the case.

1

There are 1 best solutions below

8
Mike 'Pomax' Kamermans On

We can get the combined rotation matrix by using the computed transform property for the "c" element and all its ancestors, and then matrix multiplying those (bearing in mind that CSS uses homogeneous coordinate notation rather than specifying the 2D RT matrix as a 3x3 matrix the way we normally do when we need to perform linear algebra).

This gets us to the total rotation, plus a translation that's "wrong", but that we're going to set to zero anyway, because we can get the true translation we need by using getBoundingClientRect.

The takes box "c" and calculates what the corresponding absolute transform should be, and overlays a new div "d" (absolutely positioned wrt the page itself) on top of "c". In this snippet, "c" has been given a bright red background, "d" has been given a faint, transparent blue background, and so if all went well, you should see "d" overlaid on "c" with a purplish colour.

Also note that the tops of each box have been highlight in pink, to highlight the applied rotations.

// Make sure "d" has the same dimensions as "c"
const cstyle = getComputedStyle(c);
const h = parseFloat(cstyle.height);
const w = parseFloat(cstyle.width);
d.style.height = `${h}px`;
d.style.width = `${w}px`;

// Then, align "d" to "c" using their center points.
const bbox = c.getBoundingClientRect();
const cy = bbox.top + bbox.height/2;
const cx = bbox.left + bbox.width/2;
d.style.top = `${cy - h/2}px`;
d.style.left = `${cx - w/2}px`;

// And finally, compute the total rotation, strip
// the translation, and apply that total rotation to "d".
const fullMatrix = getFullMatrix(c);
fullMatrix.e = fullMatrix.f = 0;
d.style.transform = fullMatrix;

function getFullMatrix(element) {
  if (!element) return new DOMMatrix();
  const css = getComputedStyle(element).transform;
  const matrix = new DOMMatrix(css);
  return getFullMatrix(element.offsetParent).multiply(matrix);
}
div {
  border-top: 2px solid magenta;
}

#a {
  position:relative;
  background: lightgrey;
  width: 10em;
  height: 7em;
  padding: 20px;
  margin-left: 20px;
  margin-top: 1em;
}

#b {
  position:absolute;
  background: grey;
  width: 5em;
  height: 3em;
  top: 2em;
  left: 2em;
  transform-origin: 50% 50%;
  transform: rotate(30deg);
  padding: 10px;
  margin-top: 10px;
}


#c {
  width: 2.2em;
  height: 1.8em;
  background: red;
  transform-origin: 50% 50%;
  transform: translate(1.5em, 0.75em) rotate(127deg);
}

#d {
  position: absolute;
  background: rgba(0,0,250,0.5);
}
<div id="a">
  <div id="b">
    <div id="c"></div>
  </div>
</div>

<div id="d"></div>

Note that we can of course also just compute the rotation matrices by using a slice(0,4) and computing multiplication of the 2x2 rotation matrices, but that's frankly a premature optimization (and we'll still need to set add the 0 translation terms). We can always pare things down later if needed.

Also note that IEEE floating point numbers are not our friend, and the limited decimal places of the rotation matrix, as well as fractional pixel values for bounding boxes, means that we might be half a pixel off, which can get rendered as being an entire pixel off. This is, unfortunately, a consequence of the medium. If we wanted the true angle and offset, then really we should be using CSS variables so we can work directly with the numbers that define our layout instead of working backwards.

For example:

:root {
  --px-per-em: 16;
}

div {
  --x: 0;
  --y: 0;
  --angle: 0;
  --rotation: calc(1deg * var(--angle));
  transform:
    translate(calc(1px * var(--x)), calc(1px * var(--y)))
    rotate(var(--rotation));
}

#a {
  ...
}

#b {
  --angle: 30;
  ...
}


#c {
  --x: calc(1.5 * var(--px-per-em));
  --y: calc(0.75 * var(--px-per-em));
  --angle: 127;
  ...
}

We can now use getComputedStyle(element).getPropertyValue(`--angle`) | 0; (with the same type of recursion) to get the true total angle we're using, rather than working our way back through a bunch of matrix multiplications.