有没有办法在应用 css 变换矩阵之前计算元素的结束位置?

Is there a way calculate the ending position of an element before css transform matrix is applied?

我需要在应用 css transform: matrix() 转换之前确定元素的结束边界矩形。我真的不知道从哪里开始,也找不到解决这个问题的好文章。

所以假设你有元素的原始位置 getBoundingClientRect() 如果你有应该应用的矩阵,是否有可靠的方法找到结束位置。

我构建了一个滚动控制器,我正在尝试通过屏幕映射元素进度,但我需要元素的开始和结束位置。因此,在应用 css 转换后,我需要元素的位置来确定元素何时离开屏幕。现在我只是应用矩阵,然后再次 运行ning getBoundingClientRect()。但这似乎有点老套。

假设您有原始矩阵和结束矩阵。然后你在元素上 运行 getBoundingClientRect() 找到它的位置。有没有数学方法来计算新的边界矩形?

所以对于这个例子,我们将只使用一个 6 值矩阵。但也可以将其应用于完整的 16 值矩阵:

const boundingRect = element.getBoundingClientRect();
const startingMatrix = [1, 0, 0, 1, 0, 0];
const endingMatrix = [2, 1, -1, 2, 200, 400];

// Now calculate the new bounding rect of element after ending matrix is applied.

我根据下面评论推荐的这篇文章https://dev.opera.com/articles/understanding-the-css-transforms-matrix/尝试了以下方法。有些东西我显然遗漏了或不明白。这是我的尝试:

const applyToPoint = (matrix, point) => {
  const multiplied = [
    matrix[0] * point[0],
    matrix[1] * point[1],
    matrix[2] * point[0],
    matrix[3] * point[1],
    matrix[4] * point[0],
    matrix[5] * point[1]
  ]

  const result = [
    multiplied[0] + multiplied[2] + multiplied[4],
    multiplied[1] + multiplied[3] + multiplied[5]
  ]

  return result
}

const box = document.querySelector('.box')

const startingMatrix = [1, 0, 0, 1, 0, 0]
box.style.transform = `matrix(1,0,0,1,0,0)`
const startingRect = box.getBoundingClientRect()

const endingMatrix = [2, 1, -1, 2, 200, 400]
box.style.transform = `matrix(2,1,-1,2,200,400)`
const endingRect = box.getBoundingClientRect()

const newPoint = applyToPoint(endingMatrix, [startingRect.x, startingRect.y])

console.log(newPoint, endingRect)
.box {
  height: 50px;
  width: 50px;
  background: green;
  margin: 50px 0;
}
<div class="box"></div>

我也试过了:

const applyToPoint = (matrix, point) => [
  matrix[0] * point[0] + matrix[2] * point[1] + matrix[4],
  matrix[1] * point[0] + matrix[3] * point[1] + matrix[5]
]

如有任何帮助,我们将不胜感激。或者指出正确的方向。

...to figure out when the element has left the screen.

如果您想在元素进入或退出视图时执行某些操作,那么 Intersection Observer 是更好的选择。

以整页模式查看以下代码段。

let observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting)
      log.textContent = entry.target.textContent;
  })
});

// set elements to observe
observer.observe(one);
observer.observe(two);

down.addEventListener('click', () => two.scrollIntoView({
  behavior: "smooth",
  block: "end",
  inline: "nearest"
}));
body {
  position: relative;
  height: calc(150vh + 25px);
}

p {
  position: sticky;
  top: 1rem;
}

p #log {
  background-color: yellow;
}

#down {
  position: fixed;
  top: 2rem;
  right: 2rem;
  background-color: skyblue;
  padding: .3rem;
  border-radius: 50%;
  cursor: pointer;
  user-select: none;
}

div {
  height: 50px;
  width: 50px;
  background-color: wheat;
  position: absolute;
  left: 50vw;
}

#one {
  background-color: rgb(250, 165, 165);
  top: 50vh;
  transform: translate(-50%, -50%);
}

#two {
  background-color: lime;
  top: 100%;
  transform: translateX(-50%);
}
<span id="down"></span>
<p>Element in view: <span id="log"></span></p>
<div id="one">One</div>
<div id="two">Two</div>

API 是可配置的,您甚至可以看到有多少元素可见。

问题是您混合了两个不同的坐标系:

  • 客户端系统(被getBoundingClientRect()使用)
  • 本地系统(被transform使用)

linked article 中所述, 本地系统的原点是元素的中心:

When a transform is applied to an object, it creates a local coordinate system. By default, the origin — the (0,0) point — of the local coordinate system lies at the object's center

让我们调用 [xc, yc] 元素的中心。

要转换一个点(在客户端系统中表示),您需要执行以下操作:

  • 将坐标从客户端转换为本地系统(通过减去 xcyc
  • 应用变换矩阵
  • 将生成的坐标从本地转换回客户端(通过添加 xcyc

这是一个转换 4 个框角并计算相关边界框的代码:

var matrix = [2, 1, -1, 2, 200, 400];

var box = document.querySelector('.box');
var startingRect = box.getBoundingClientRect();
var xc = startingRect.left + startingRect.width/2;
var yc = startingRect.top + startingRect.height/2;

const applyMatrix = (matrix, point) => [
  matrix[0] * point[0] + matrix[2] * point[1] + matrix[4],
  matrix[1] * point[0] + matrix[3] * point[1] + matrix[5]
];
const clientToLocal = (point) => [point[0] - xc, point[1] - yc];
const localToClient = (point) => [point[0] + xc, point[1] + yc];
const transformPoint = (point) => localToClient(applyMatrix(matrix, clientToLocal(point)));

function getBoundingBox(pt1, pt2, pt3, pt4)
{
    var x1 = Math.min(pt1[0], pt2[0], pt3[0], pt4[0]);
    var y1 = Math.min(pt1[1], pt2[1], pt3[1], pt4[1]);
    var x2 = Math.max(pt1[0], pt2[0], pt3[0], pt4[0]);
    var y2 = Math.max(pt1[1], pt2[1], pt3[1], pt4[1]);
    return {x: x1, y: y1, width: x2 - x1, height: y2 - y1};
}

var topLeft = [startingRect.left, startingRect.top];
var topRight = [startingRect.right, startingRect.top];
var bottomLeft = [startingRect.left, startingRect.bottom];
var bottomRight = [startingRect.right, startingRect.bottom];
var transformedBox = getBoundingBox(transformPoint(topLeft), transformPoint(topRight), transformPoint(bottomLeft), transformPoint(bottomRight));
console.log(transformedBox);

box.style.transform = 'matrix('+matrix.join()+')';
var endingRect = box.getBoundingClientRect();
console.log(endingRect);

输出:

{x: 158, y: 400, width: 150, height: 150}
DOMRect {x: 158, y: 400, width: 150, height: 150, top: 400, …}

如评论中所述,您必须简单地乘以矩阵(您可以只做 2 x 2 版本,然后添加平移变量。)但正如 Olivier 所述,您需要先平移坐标,然后将它们翻译回去。这很简单,只需计算中心即可。

我会把它写成一个函数,从一个矩形和矩阵(作为用于 CSS 转换的六个变量的平面数组)到另一个矩形,一个简单的 {left, right, top, bottom} 矩形或如果需要,一个 DomRect。

您可以在此代码段中看到它(如果使用“整页”展开它会更容易看到 link):

const transformRect = (rect, matrix) => {
  const {left, top, right, bottom} = rect
  const [a, b, c, d, tx, ty] = matrix
  const dx =  (left + right) / 2, dy = (top + bottom) / 2
  const newCorners = [[left, top], [right, top], [right, bottom], [left, bottom]]
    .map (([x, y]) => [
      a * (x - dx) + c * (y - dy) + tx + dx, 
      b * (x - dx) + d * (y - dy) + ty + dy
    ])
  const _left = Math .min (... newCorners .map (p => p [0]))
  const _right = Math .max (... newCorners .map (p => p [0]))
  const _top = Math .min (... newCorners .map (p => p [1]))
  const _bottom = Math .max (... newCorners .map (p => p [1]))

  return DOMRect.fromRect (
    {x: _left, y: _top, width: _right - _left, height: _bottom - _top}
  ) // or just
 // return {x: _left, y: _top, width: _right - _left, height: _bottom - _top}
}

const div = document .getElementById ('d2') 
const rect = div.getBoundingClientRect()
console .log ('Before:' , rect)

const matrix = [2, 1, -1, 2, 200, 400]
div.style.transform = `matrix(${matrix .join (', ')})`

console .log ('After:', transformRect (rect, matrix))
.box {
  height: 50px;
  width: 50px;
  background: green;
  color: white;
}
#d1 {
  background: #ccc;
  position: absolute;
  top: 8;
  left: 8;
}
<div id="d1" class="box">shadow</div>
<div id="d2" class="box">content</div>