如何在不考虑转换的情况下计算 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(不包括页边距)
编辑:答案汇总
获取元素位置而不考虑元素上的任何变换和 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
)
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(不包括页边距)
编辑:答案汇总
获取元素位置而不考虑元素上的任何变换和 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
)