如何在不考虑转换的情况下计算 getBoundingClientRect()?

How to compute getBoundingClientRect() without considering transforms?

getBoundingClientRect() returns 元素在屏幕上的坐标 变换之后。如何计算那些坐标 before 被转换?即没有转换。

我找到的最简单的方法是:

element.style.transform = 'none'; //temporarily reset the transform
var untransformedOffset = element.getBoundingClientRect().top; //get the value
element.style.transform = ''; //set it back

但这会导致缓慢的布局抖动,如果在许多元素上完成时尤其明显。现场演示:http://jsbin.com/nibiqogosa/1/edit?js,console,output

有没有更好的方法?


javascript 代码可应用于:

<div id="element"></div>
<style> #element { transform: translateY(20px); }</style>

结果将为 0(不包括页边距)

element.getBoundingClientRect().top的结果为20(不包括页边距)

编辑:答案汇总

http://jsbin.com/kimaxojufe/1/edit?css,js,console,output

获取元素位置而不考虑元素上的任何变换和 DOM 树:

var el = element,
offsetLeft = 0,
offsetTop  = 0;

do{
    offsetLeft += el.offsetLeft;
    offsetTop  += el.offsetTop;

    el = el.offsetParent;
} while( el );

获取元素位置而不考虑对其应用的变换,但保留 DOM 树上的任何变换。

为此,您可以尝试恢复转换。
您必须首先将 transform-origin 设置为 0,0,0 并围绕您的变换(缩放、旋转)宽度 translate(50%,50%) ... translate(-50%, -50%)。 这是一个例子,
改变那个:

transform: scale(2) rotate(45deg) translate(20px);
transform-origin: 50% 50%; //default value

进入

transform: translate(50%, 50%) scale(2) rotate(45deg) translate(-50%,-50%) translate(20px);
transform-origin: 0 0 0;

我们需要这样做,因为 getComputedStyle() 返回的矩阵不包括使用 transform-origin 完成的内容。真的不知道为什么。

然后您可以使用此代码:

function parseTransform(transform){
    //add sanity check
    return transform.split(/\(|,|\)/).slice(1,-1).map( function(v){
        return parseFloat(v);
    });
}

function convertCoord(transformArr, x, y, z){
    //add sanity checks and default values      

    if( transformArr.length == 6 ){
        //2D matrix
        //need some math to apply inverse of matrix
        var t = transformArr,
            det = t[0]*t[3] - t[1]*t[2];
        return {
            x: (  x*t[3] - y*t[2] + t[2]*t[5] - t[4]*t[3] )/det,
            y: ( -x*t[1] + y*t[0] + t[4]*t[1] - t[0]*t[5] )/det
        }
    }
    else /*if (transformArr.length > 6)*/{
       //3D matrix
       //haven't done the calculation to apply inverse of 4x4 matrix
    }
}

var elRect = element.getBoundingClientRect(),
    st = window.getComputedStyle(element),

    topLeft_pos = convertCoord(
              parseTransform( st.transform ),
              elRect.left,
              elRect.top,
              st.perspective
    );    

我不会解释数学部分,因为我认为它超出了本文的范围 post。仍然可以在其他地方解释它(也许是另一个问题?)。

我喜欢 Ghetolay 的回答。我使用了它,但通过避免循环使它的性能提高了一点。

我有一个可拖动的标签云,我必须使用变换更新拖动位置,但要跟踪原始位置(没有变换)。

之前的答案建议循环遍历 offsetParents。在我的例子中,我认为在很多情况下,标签被转换但容器没有。所以我只需要得到第一个 offsetParent 并在那里使用 getBoundingClientRect() 。不需要一直循环。我这样做解决了它:

var el = element;
var parentRect = element.offsetParent.getBoundingClientRect();
var offsetLeft = parentRect.left + element.offsetLeft;
var offsetTop = parentRect.top + element.offsetTop;

上面以数学方式反转变换的答案是一个很好的尝试,但不是很正确(并且比需要的更复杂)。下面是更正确的反转。

这不考虑倾斜或旋转平移,但至少在使用缩放时它会产生正确的边缘位置,并且在没有变换时不会造成太大的性能损失。

即使使用 scale(0),它也能产生准确的结果(尽管在 width/height 上会失去亚像素精度)。

请注意,打开软件键盘时 iOS 会在 getBoundingClientRect()offsetTop/offsetLeft 之间产生不同的结果 - 后者在任何浏览器上都不支持亚像素精度.这会产生与 getBoundingClientRect().

一致的结果

function adjustedBoundingRect(el) {
  var rect = el.getBoundingClientRect();
  var style = getComputedStyle(el);
  var tx = style.transform;

  if (tx) {
    var sx, sy, dx, dy;
    if (tx.startsWith('matrix3d(')) {
      var ta = tx.slice(9,-1).split(/, /);
      sx = +ta[0];
      sy = +ta[5];
      dx = +ta[12];
      dy = +ta[13];
    } else if (tx.startsWith('matrix(')) {
      var ta = tx.slice(7,-1).split(/, /);
      sx = +ta[0];
      sy = +ta[3];
      dx = +ta[4];
      dy = +ta[5];
    } else {
      return rect;
    }

    var to = style.transformOrigin;
    var x = rect.x - dx - (1 - sx) * parseFloat(to);
    var y = rect.y - dy - (1 - sy) * parseFloat(to.slice(to.indexOf(' ') + 1));
    var w = sx ? rect.width / sx : el.offsetWidth;
    var h = sy ? rect.height / sy : el.offsetHeight;
    return {
      x: x, y: y, width: w, height: h, top: y, right: x + w, bottom: y + h, left: x
    };
  } else {
    return rect;
  }
}

var div = document.querySelector('div');
console.log(div.getBoundingClientRect(), adjustedBoundingRect(div));
div.classList.add('transformed');
console.log(div.getBoundingClientRect(), adjustedBoundingRect(div));
.transformed {
  transform: translate(8px,8px) scale(0.5);
  transform-origin: 16px 16px;
}
<div>Hello</div>

改进 Ghetolay 的回答:

获取元素位置而不考虑元素上的任何变换和 DOM 树:

function getOffset(element)
{
  var offsetLeft = 0;
  var offsetTop  = 0;
  
  while (element)
  {
      offsetLeft += element.offsetLeft;
      offsetTop  += element.offsetTop;
  
      element = element.offsetParent;
  }
  
  return [offsetLeft, offsetTop];
}

改进:

  • 包含在函数中
  • loop 将无错误地处理空元素 (element = null/undefined)