Lorenzo Maieru Lorenzo Maieru - 7 months ago 22
HTML Question

get real 2D vertex coordinates of a div after CSS 3D transformations with Javascript

I've been trying to figure this out for a couple of days now but I can't seem to get it right.

Basically, I have some divs whose parent has a CSS perspective and rotateX 3D transformations applied and I need to get the actual on-screen coordinates of those divs.

Here's a jsfiddle with an example of what I mean (albeit not working properly).

https://jsfiddle.net/6ev6d06z/3/

As you can see, the vertexes are off (thanks to the transformations of its parents)

I've tried using

getBoundingClientRect()


but that doesn't seem to be taking the 3D transforms into consideration.
I don't know if there's an already established method to get what I need but otherwise I guess there must be a way of calculating the coordinates using the matrix3D.

Any help is appreciated.

Answer

As it is there is not a builtin way to get the actual 2d coordinates of each vertex of the transformed element. In the case of all the APIs (such as getBoundingClientRect), they return a bounding rectangle of the transformed element represented as a 2point rectangle [(top,left), (bottom,right)].

That being said, you can absolutely get the actual coordinates with a little bit of effort and matrix math. The easiest thing to do would be to use a premade matrix library to do the math (I've head good things about math.js but have not used it), although it is certainly doable yourself.

In pseudo-code for what you will need to do:

  1. Get the untransformed bounds of the transformed parent element in the document coordinate system.
  2. Get the untransformed bounds of the target element in the document coordinate system.
  3. Compute the target's untransformed bounds relative to the parent's untransformed bounds. a. Subtract the top/left offset of (1) from the bounds of (2).
  4. Get the css transform of the parent element.
  5. Get the transform-origin of the parent element (defaults to (50%, 50%)).
  6. Get the actual applied transform (-origin * css transform * origin)
  7. Multiply the four vertices from (3) by the computed transform from (6).
  8. Perform the homogeneous divide (divide x, y, z by the w component) to apply perspective.
  9. Transform the projected vertices back into the document coordinate system.
  10. Fun!

And then for fun in real code: https://jsfiddle.net/cLnmgvb3/1/

$(".target").on('click', function(){
    $(".vertex").remove();

    // Note: The 'parentOrigin' and 'rect' are computed relative to their offsetParent rather than in doc
    //       coordinates. You would need to change how these offsets are computed to make this work in a
    //       more complicated page. In particular, if txParent becomes the offsetParent of 'this', then the
    //       origin will be wrong.

    // (1) Get the untransformed bounds of the parent element. Here we only care about the relative offset
    //     of the parent element to its offsetParent rather than it's full bounding box. This is the origin
    //     that the target elements are relative to.
    var txParent = document.getElementById('transformed');

    var parentOrigin = [ txParent.offsetLeft, txParent.offsetTop, 0, 0 ];
    console.log('Parent Origin: ', parentOrigin);

    // (2) Get the untransformed bounding box of the target elements. This will be the box that is transformed.
    var rect = { left: this.offsetLeft, top: this.offsetTop, right: this.offsetLeft + this.offsetWidth, bottom: this.offsetTop + this.offsetHeight };

    // Create the vertices in the coordinate system of their offsetParent - in this case <body>.
    var vertices =
        [
            [ rect.left, rect.top, 0, 1 ],
            [ rect.right, rect.bottom, 0, 1 ],
            [ rect.right, rect.top, 0, 1 ],
            [ rect.left, rect.bottom, 0, 1 ]
        ];
    console.log('Original: ', vertices);

    // (3) Transform the vertices to be relative to transformed parent (the element with
    //     the CSS transform on it).
    var relVertices = [ [], [], [], [] ];
    for (var i = 0; i < 4; ++i)
    {
        relVertices[i][0] = vertices[i][0] - parentOrigin[0];
        relVertices[i][1] = vertices[i][1] - parentOrigin[1];
        relVertices[i][2] = vertices[i][2];
        relVertices[i][3] = vertices[i][3];
    }

    // (4) Get the CSS transform from the transformed parent
    var tx = getTransform(txParent);
    console.log('Transform: ', tx);

    // (5) Get the CSS transform origin from the transformed parent - default is '50% 50%'
    var txOrigin = getTransformOrigin(txParent);
    console.log('Transform Origin: ', txOrigin);

    // (6) Compute the full transform that is applied to the transformed parent (-origin * tx * origin)
    var fullTx = computeTransformMatrix(tx, txOrigin);
    console.log('Full Transform: ', fullTx);

    // (7) Transform the vertices from the target element's bounding box by the full transform
    var txVertices = [ ];
    for (var i = 0; i < 4; ++i)
    {
        txVertices[i] = transformVertex(fullTx, relVertices[i]);
    }

    console.log('Transformed: ', txVertices);

    // (8) Perform the homogeneous divide to apply perspective to the points (divide x,y,z by the w component).
    var projectedVertices = [ ];
    for (var i = 0; i < 4; ++i)
    {
        projectedVertices[i] = projectVertex(txVertices[i]);
    }

    console.log('Projected: ', projectedVertices);

    // (9) After the transformed vertices have been computed, transform them back into the coordinate
    // system of the offsetParent.
    var finalVertices = [ [], [], [], [] ];
    for (var i = 0; i < 4; ++i)
    {
        finalVertices[i][0] = projectedVertices[i][0] + parentOrigin[0];
        finalVertices[i][1] = projectedVertices[i][1] + parentOrigin[1];
        finalVertices[i][2] = projectedVertices[i][2];
        finalVertices[i][3] = projectedVertices[i][3];
    }

    // (10) And then add the vertex elements in the 'offsetParent' coordinate system (in this case again
    //      it is <body>).
    for (var i = 0; i < 4; ++i)
    {
       $("<div></div>").addClass("vertex")
          .css('position', 'absolute')
          .css('left', finalVertices[i][0])
          .css('top', finalVertices[i][1])
          .appendTo('body');
    }
  });

function printMatrix(mat)
{
    var str = '';
    for (var i = 0; i < 4; ++i)
    {
        for (var j = 0; j < 4; ++j)
        {
            str += (' ' + mat[i][j]);
        }

        str += '\r\n';
    }

    console.log(str);
}

function getTransform(ele)
{
    var st = window.getComputedStyle(ele, null);

    var tr = st.getPropertyValue("-webkit-transform") ||
             st.getPropertyValue("-moz-transform") ||
             st.getPropertyValue("-ms-transform") ||
             st.getPropertyValue("-o-transform") ||
             st.getPropertyValue("transform");

    var values = tr.split('(')[1],
    values = values.split(')')[0],
    values = values.split(',');

    var mat = [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ];    
    if (values.length === 16)
    {
        for (var i = 0; i < 4; ++i)
        {
            for (var j = 0; j < 4; ++j)
            {
                mat[j][i] = +values[i * 4 + j];
            }
        }
    }
    else
    {
        for (var i = 0; i < 3; ++i)
        {
            for (var j = 0; j < 2; ++j)
            {
                mat[j][i] = +values[i * 2 + j];
            }
        }
    }

    return mat;
}

function getTransformOrigin(ele)
{
    var st = window.getComputedStyle(ele, null);

    var tr = st.getPropertyValue("-webkit-transform-origin") ||
             st.getPropertyValue("-moz-transform-origin") ||
             st.getPropertyValue("-ms-transform-origin") ||
             st.getPropertyValue("-o-transform-origin") ||
             st.getPropertyValue("transform-origin");

    var values = tr.split(' ');

    var out = [ 0, 0, 0, 1 ];
    for (var i = 0; i < values.length; ++i)
    {
        out[i] = parseInt(values[i]);
    }    

    return out;
}

function createTranslateMatrix(x, y, z)
{
    var out = 
    [
        [1, 0, 0, x],
        [0, 1, 0, y],
        [0, 0, 1, z],
        [0, 0, 0, 1]
    ];

    return out;
}

function multiply(pre, post)
{
    var out = [ [], [], [], [] ];

    for (var i = 0; i < 4; ++i)
    {       
        for (var j = 0; j < 4; ++j)
        {
            var sum = 0;

            for (var k = 0; k < 4; ++k)
            {
                sum += (pre[k][i] * post[j][k]);
            }

            out[j][i] = sum;
        }
    }

    return out;
}

function computeTransformMatrix(tx, origin)
{
   var out;

   var preMul = createTranslateMatrix(-origin[0], -origin[1], -origin[2]);
   var postMul = createTranslateMatrix(origin[0], origin[1], origin[2]);

   var temp1 = multiply(preMul, tx);

   out = multiply(temp1, postMul);

   return out;
}

function transformVertex(mat, vert)
{
   var out = [ ];

    for (var i = 0; i < 4; ++i)
    {
        var sum = 0;
        for (var j = 0; j < 4; ++j)
        {
            sum += +mat[i][j] * vert[j];
        }

        out[i] = sum;
    }

   return out;
}

function projectVertex(vert)
{
    var out = [ ];

    for (var i = 0; i < 4; ++i)
    {
        out[i] = vert[i] / vert[3];
    }

    return out;
}