offsetX/Y + matrix3d = bad

When hovering over a Slot, the offsetX/Y of the 'pointermove' event becomes relative to the slot, regardless of where the event is listened from! This is easy to fix in a flat app, but more complex in a transformed one!

by David Iglesias

HTML

<div class="transform">
  <div class="app">
    <div class="custom-content" slot="content">
      Hover over this, <tt>(x, y)</tt> will reset to 0,0!
    </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>

CSS

* { box-sizing: border-box; }
body { font: 14px sans-serif; }

.transform {
  transform: matrix3d(0.498701, -0.26373, 0,  0.000148,
    0.303401, 0.323287, 0, -0.000143,
    0,        0, 1,         0,
    20,      20, 0,         1);

}

.app {
  background: #fbd;
  width: 320px;
  height: 200px;
  border: 1px 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;
}

.fixed {
  text-decoration: line-through;
}

JavaScript

function computeCoordinates(event) {
  // event.offsetX/Y are "wrong" for slots
  let x = event.offsetX;
  let y = event.offsetY;

  return {x: x, y: y};
}


// Setup the app
var app = document.querySelector('.app');
let shadowRoot = app.attachShadow({
  mode: 'open',
});

let slot = document.createElement('slot');
slot.name = 'content';
shadowRoot.appendChild(slot);

// Event handlers
app.addEventListener('pointermove', function(event) {
  let coords = computeCoordinates(event);
  updateOutput(coords);  
}, { capture: false });

app.addEventListener('pointerleave', function(event) {
  event.target.classList.remove('active');
}, { capture: true });

app.addEventListener('pointerenter', function(event) {
  event.target.classList.add('active');
}, { capture: true });

function updateOutput(info) {
  output.innerText = `x: ${Math.round(info.x)}\ny: ${Math.round(info.y)}`;
}