art with code

2015-10-28

Mouse event coordinates on CSS transformed elements

How to turn mouse event coordinates into element-relative coordinates when the element has CSS transforms applied to it? Conceptually it's simple. You need to get the layerX and layerY of the mouse event, then transform those with the CSS transforms. The implementation is a bit tricky.

The following snippet is what I'm using with Three.js to convert renderer.domElement click coordinates to a mouse3D vector used for picking. If you just need the pixel x/y coords on the element, skip the mouse3D part.

// First get the computed transform and transform-origin of the event target.
var style = getComputedStyle(ev.target);
var elementTransform = style.getPropertyValue('transform');
var elementTransformOrigin = style.getPropertyValue('transform-origin');

// Convert them into Three.js matrices
var xyz = elementTransformOrigin.replace(/px/g, '').split(" ");
xyz[0] = parseFloat(xyz[0]);
xyz[1] = parseFloat(xyz[1]);
xyz[2] = parseFloat(xyz[2] || 0);

var mat = new THREE.Matrix4();
mat.identity();
if (/^matrix\(/.test(elementTransform)) {
  var elems = elementTransform.replace(/^matrix\(|\)$/g, '').split(' ');
  mat.elements[0] = parseFloat(elems[0]);
  mat.elements[1] = parseFloat(elems[1]);
  mat.elements[4] = parseFloat(elems[2]);
  mat.elements[5] = parseFloat(elems[3]);
  mat.elements[12] = parseFloat(elems[4]);
  mat.elements[13] = parseFloat(elems[5]);
} else if (/^matrix3d\(/i.test(elementTransform)) {
  var elems = elementTransform.replace(/^matrix3d\(|\)$/ig, '').split(' ');
  for (var i=0; i<16; i++) {
    mat.elements[i] = parseFloat(elems[i]);
  }
}

// Apply the transform-origin to the transform.
var mat2 = new THREE.Matrix4();
mat2.makeTranslation(xyz[0], xyz[1], xyz[2]);
mat2.multiply(mat);
mat.makeTranslation(-xyz[0], -xyz[1], -xyz[2]);
mat2.multiply(mat);

// Multiply the event layer coordinates with the transformation matrix.
var vec = new THREE.Vector3(ev.layerX, ev.layerY, 0);
vec.applyMatrix4(mat2);

// Yay, now vec.x and vec.y are in element coordinate system.


// Optional: get the untransformed width and height of the element and
// divide the mouse coords with those to get normalized coordinates.

var width = parseFloat(style.getPropertyValue('width'));
var height = parseFloat(style.getPropertyValue('height'));

var mouse3D = new THREE.Vector3(
 ( vec.x / width ) * 2 - 1,
 -( vec.y / height ) * 2 + 1,
 0.5
);

There you go. A bit of a hassle, but tractable.

Blog Archive