fromScreenToElement

Converts points from event.pageX/Y to the corresponding offsetX/Y of a transformed element.

by David Iglesias

HTML

<div class="bad_transform">
  <div class="transform">
    <div class="app">
    <!-- This has a shadowRoot -->
    <!-- irrelevant for the problem
    <div class="custom-content" slot="content">
      Hover over this, <tt>(x, y)</tt> will reset to 0,0!
    </div>
    -->
    </div>
  </div>
</div>
<!-- Force scroll and render debug info -->
<div class="far">
  And here, some content so the above can scroll!
</div>

<div id="output">x: ?<br />y: ?</div>

<div id="graphicalDebug"></div>

CSS

* { box-sizing: border-box; transform-origin: 0 0 0; margin: 0px; padding: 0px; }
body { font: 14px sans-serif; margin: 0; }
/*
.bad_transform {
  transform: translate(60px, 130px) rotateX(60deg) rotateZ(-45deg);
}
/*/
.transform {
  transform: matrix3d(0.707107, -0.26373, -0,  0.000148,
                      0.707107, 0.323287,  0, -0.000143,
                             0,-0.866025,  1,         0,
                            60,      130,  0,         1);

}
/**/
.app {
  background: #fbd;
  width: 320px;
  height: 200px;
  border: 0px solid black;
  position: relative;
  cursor: move;
  transform-style: preserve-3d;
}

.custom-content {
  display: block;
  border: 1px solid black;
  background: #dbf;
  width: 100px;
  height: 100px;
  position: absolute;
  top: 50%; margin-top: -50px;
  left: 50%; margin-left: -50px;
  padding: 5px;
  cursor: crosshair;
  transform: perspective(500px) rotateY(45deg) translateZ(30px);
}

#output {
  font-family: monospace;
  background: #eee;
  position: fixed;
  bottom: 0; left: 0; right: 0;
  border-top: 1px solid #ddd;
}

.active {
  border-color: red;
}

.far {
  margin-top: 500px;
}

#graphicalDebug {
  pointer-events: none;
  position: absolute;
  width: 100%;
  height: 100%;
  overflow: hidden;
  top: 0px;
  left: 0px;
}

custom-point {
  position: absolute;
  display: block;
  width: 10px;
  height: 10px;
  margin-left: -5px; margin-top: -5px;
  border-radius: 5px;
  pointer-events: none;
  opacity: .5;
}

custom-rect {
  position: absolute;
  display: block;
  border: 1px solid;
  overflow: hidden;
  pointer-events: none;
  opacity: .5;
}

JavaScript

// Current API reimplementation of:
// https://gist.github.com/Yaffle/1145197
// This code works if the 4th column of the matrix3d (perspective) is set to (0, 0, 0, 1)
// Reading: https://www.scratchapixel.com/lessons/3d-basic-rendering/computing-pixel-coordinates-of-3d-point
// More: https://dev.opera.com/articles/understanding-the-css-transforms-matrix
// AKA: convertPointFromPageToNode
let identity = new DOMMatrixReadOnly();

// Recursively computes the perspective matrix from `element` to the 
// root of the page.
// If E is the element and P, GP, GGP are its Parent/GrandParent/GreatGrandPa
// This function composes the transform matrix like so:
// GPP*(GP*(P*E)) by multiplying matrices from left to right as the algo
// walks up the DOM tree.
function getCssTransforms(element) {
  let current = element;
  let transforms = identity;
  while (current != null && current !== current.ownerDocument.documentElement) {
    let computedStyle = window.getComputedStyle(current);
    let transform = new DOMMatrixReadOnly(computedStyle.transform);
    let origin = computedStyle.transformOrigin;
		if (!transform.isIdentity) {
      transforms = transform.multiply(transforms);
    }
    current = current.parentNode;
  }
  return transforms;
}

// Computes the transform matrix applied to `element`, including a global
// translation before applying the perspective matrix `transforms`.
function getTransformMatrix(element, transforms) {
  // Attempt to figure out the transformed top-left coordinates
  // of `element` to compute the translation applied from the
  // outside? (margins, paddings, and friends?)
  let w = element.offsetWidth; // post transform
  let h = element.offsetHeight; // post transform
  let p1 = transforms.transformPoint(new DOMPoint(0, 0));
  let p2 = transforms.transformPoint(new DOMPoint(w, 0));
  let p3 = transforms.transformPoint(new DOMPoint(w, h));
  let p4 = transforms.transformPoint(new DOMPoint(0, h));

  let left = Math.min(p1.x/p1.w, p2.x/p2.w,...